@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +268 -9
- package/CHANGELOG.md +8 -0
- package/README.md +1 -0
- package/bin/apifuse-check.ts +84 -2
- package/bin/apifuse-dev.ts +11 -31
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-pack-check.ts +11 -4
- package/bin/apifuse-pack-types.ts +191 -4
- package/bin/apifuse-record.ts +44 -32
- package/bin/apifuse-test.ts +3 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +11 -0
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-provider-shape.d.ts +5 -1
- package/dist/cli/migrate-provider-shape.js +4 -2
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +5 -0
- package/dist/dev.d.ts +3 -1
- package/dist/dev.js +6 -1
- package/dist/engine.d.ts +35 -1
- package/dist/engine.js +21 -2
- package/dist/error-resolution.d.ts +1 -0
- package/dist/error-resolution.js +54 -0
- package/dist/errors.d.ts +39 -1
- package/dist/errors.js +50 -0
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/lint.d.ts +10 -0
- package/dist/lint.js +440 -57
- package/dist/provider.d.ts +7 -5
- package/dist/provider.js +4 -2
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +3 -0
- package/dist/runtime/browser.js +208 -47
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.js +67 -7
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/diagnostic-env.d.ts +3 -0
- package/dist/runtime/diagnostic-env.js +10 -1
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +356 -174
- package/dist/runtime/instrumentation.js +92 -13
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.js +30 -8
- package/dist/runtime/request-telemetry.d.ts +2 -0
- package/dist/runtime/request-telemetry.js +2 -1
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +3 -2
- package/dist/runtime/resolver-shared.js +4 -4
- package/dist/runtime/resolver-telemetry.d.ts +1 -0
- package/dist/runtime/resolver-telemetry.js +1 -1
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +46 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
- package/dist/runtime/resolver-vendors/hosts.js +35 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
- package/dist/runtime/resolver-vendors/types.d.ts +5 -4
- package/dist/runtime/resolver.d.ts +4 -1
- package/dist/runtime/resolver.js +46 -13
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +26 -7
- package/dist/runtime/stealth-cookies.d.ts +5 -1
- package/dist/runtime/stealth-cookies.js +21 -1
- package/dist/runtime/stealth-owned-headers.d.ts +26 -0
- package/dist/runtime/stealth-owned-headers.js +37 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +4 -1
- package/dist/runtime/stealth.js +228 -85
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +15 -6
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +5 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test.js +1 -18
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +9 -0
- package/dist/server/serve-implementation.d.ts +53 -2
- package/dist/server/serve-implementation.js +369 -123
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +7 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
- package/dist/stateful/stateful-provider-runtime-context.js +1 -0
- package/dist/types.d.ts +94 -1
- package/package.json +6 -1
- package/src/cli/commands.ts +14 -0
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-provider-shape.ts +10 -4
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/define.ts +13 -0
- package/src/dev.ts +9 -2
- package/src/engine.ts +38 -2
- package/src/error-resolution.ts +58 -0
- package/src/errors.ts +77 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/index.ts +91 -0
- package/src/lint.ts +552 -57
- package/src/provider.ts +20 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +249 -65
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +68 -17
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/diagnostic-env.ts +11 -1
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +457 -204
- package/src/runtime/instrumentation.ts +105 -16
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +109 -74
- package/src/runtime/request-telemetry.ts +2 -1
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +9 -4
- package/src/runtime/resolver-telemetry.ts +1 -1
- package/src/runtime/resolver-vendors/browser.ts +65 -7
- package/src/runtime/resolver-vendors/hosts.ts +43 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
- package/src/runtime/resolver-vendors/types.ts +5 -4
- package/src/runtime/resolver.ts +64 -21
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +46 -45
- package/src/runtime/stealth-cookies.ts +24 -1
- package/src/runtime/stealth-owned-headers.ts +41 -0
- package/src/runtime/stealth-telemetry.ts +304 -0
- package/src/runtime/stealth.ts +355 -150
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +93 -71
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +66 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test.ts +1 -21
- package/src/server/sensitive-values.ts +14 -0
- package/src/server/serve-implementation.ts +601 -157
- package/src/server/types.ts +7 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
- package/src/stateful/stateful-provider-runtime-context.ts +4 -0
- package/src/types.ts +99 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/dist/runtime/http.js
CHANGED
|
@@ -2,9 +2,34 @@ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, re
|
|
|
2
2
|
import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
|
|
3
3
|
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
4
4
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
5
|
+
import { httpTelemetryErrorCode, } from "./http-telemetry.js";
|
|
5
6
|
import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
|
|
6
7
|
import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
|
|
8
|
+
import { startHttpTelemetry } from "./http-telemetry-guard.js";
|
|
9
|
+
import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
|
|
7
10
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
11
|
+
function httpAttemptFailure(error, status) {
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
|
|
15
|
+
status: proxyTransportRetryErrorStatus(error) ?? status,
|
|
16
|
+
...(error instanceof Error
|
|
17
|
+
? {
|
|
18
|
+
diagnostics: {
|
|
19
|
+
name: error.name,
|
|
20
|
+
message: error.message,
|
|
21
|
+
...(error.cause instanceof Error
|
|
22
|
+
? { cause: { name: error.cause.name, message: error.cause.message } }
|
|
23
|
+
: {}),
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { e: "other" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
8
33
|
function isHttpStatusOutcome(outcome) {
|
|
9
34
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
10
35
|
}
|
|
@@ -59,19 +84,92 @@ function hasHeader(headers, name) {
|
|
|
59
84
|
const needle = name.toLowerCase();
|
|
60
85
|
return Object.keys(headers).some((key) => key.toLowerCase() === needle);
|
|
61
86
|
}
|
|
62
|
-
function
|
|
63
|
-
const
|
|
87
|
+
function withClientHeaderDefaults(headers, clientOptions, body) {
|
|
88
|
+
const merged = {
|
|
64
89
|
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
65
|
-
...
|
|
90
|
+
...headers,
|
|
66
91
|
};
|
|
67
|
-
if (body !== undefined && !hasHeader(
|
|
68
|
-
|
|
92
|
+
if (body !== undefined && !hasHeader(merged, "Content-Type")) {
|
|
93
|
+
merged["Content-Type"] = "application/json";
|
|
69
94
|
}
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
function withClientHeaders(options, clientOptions, body) {
|
|
98
|
+
// The ambient client signal is merged into the per-request one here so every
|
|
99
|
+
// downstream read (retry loop, sleep, body consumption) sees a single signal.
|
|
100
|
+
// The header-factory branch needs it too, otherwise factory-header requests
|
|
101
|
+
// would lose ambient cancellation entirely.
|
|
102
|
+
const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
|
|
103
|
+
// A header factory is resolved per issued attempt (resolveAttemptHeaders);
|
|
104
|
+
// the client defaults are applied to its output at that point.
|
|
105
|
+
if (typeof options?.headers === "function")
|
|
106
|
+
return { ...options, signal };
|
|
70
107
|
return {
|
|
71
108
|
...options,
|
|
72
|
-
headers,
|
|
109
|
+
headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
|
|
110
|
+
signal,
|
|
73
111
|
};
|
|
74
112
|
}
|
|
113
|
+
function headerFactoryFailure(message, cause) {
|
|
114
|
+
// A dedicated code keeps a broken proof minter out of the retry predicate:
|
|
115
|
+
// toHttpTransportError would otherwise brand a plain throw as
|
|
116
|
+
// transport_network_error, which the default retry policy re-issues.
|
|
117
|
+
// retryable:false is explicit so the served error envelope does not
|
|
118
|
+
// advertise a deterministic signing failure as retryable, and
|
|
119
|
+
// category:provider_error keeps a provider-local minting bug out of the
|
|
120
|
+
// upstream-failure signal (status 0 would otherwise fall through the
|
|
121
|
+
// observability ladder to upstream_http / source upstream_failure).
|
|
122
|
+
return new TransportError(message, {
|
|
123
|
+
code: "http_header_factory_failed",
|
|
124
|
+
status: 0,
|
|
125
|
+
retryable: false,
|
|
126
|
+
category: "provider_error",
|
|
127
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function isHeaderRecord(value) {
|
|
131
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
// Settle `promise`, or reject with the signal's reason as soon as it aborts, so
|
|
134
|
+
// a pending header factory cannot outlive the request deadline or an ambient
|
|
135
|
+
// cancellation (native fetch has not started yet, so nothing else observes them).
|
|
136
|
+
function settleBeforeAbort(promise, signal) {
|
|
137
|
+
if (!signal)
|
|
138
|
+
return promise;
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const onAbort = () => reject(signal.reason);
|
|
141
|
+
if (signal.aborted)
|
|
142
|
+
onAbort();
|
|
143
|
+
else
|
|
144
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
// Always attach handlers: a late rejection after the abort must settle
|
|
146
|
+
// here (a no-op on the already-rejected promise), never go unhandled.
|
|
147
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function resolveAttemptHeaders(options, clientOptions, attempt, signal) {
|
|
151
|
+
const { headers } = options;
|
|
152
|
+
if (typeof headers !== "function")
|
|
153
|
+
return headers;
|
|
154
|
+
// The per-call timeout may have fired during proxy resolution: do not start
|
|
155
|
+
// the factory at all once the request is already aborted.
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
throw signal.reason;
|
|
158
|
+
let minted;
|
|
159
|
+
try {
|
|
160
|
+
minted = await settleBeforeAbort(Promise.resolve().then(() => headers(attempt())), signal);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// An abort is classified by toHttpTransportError (cancelled / timed out).
|
|
164
|
+
if (signal?.aborted)
|
|
165
|
+
throw error;
|
|
166
|
+
throw headerFactoryFailure("Request header factory failed", error);
|
|
167
|
+
}
|
|
168
|
+
if (!isHeaderRecord(minted)) {
|
|
169
|
+
throw headerFactoryFailure("Request header factory must return a header record");
|
|
170
|
+
}
|
|
171
|
+
return withClientHeaderDefaults(minted, clientOptions, options.body);
|
|
172
|
+
}
|
|
75
173
|
function parseHttpData(body, headers) {
|
|
76
174
|
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
77
175
|
if (contentType?.includes("application/json")) {
|
|
@@ -527,22 +625,29 @@ function serializeHttpRequestUrl(baseUrl, url, options) {
|
|
|
527
625
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
528
626
|
}
|
|
529
627
|
}
|
|
530
|
-
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
|
|
628
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy, nextHeaderAttempt = () => 1) {
|
|
531
629
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
532
630
|
const { requestUrl } = serializedUrl;
|
|
533
631
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
534
|
-
const signal = mergeAbortSignals(
|
|
535
|
-
|
|
632
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
633
|
+
let timeoutHandle = options.timeout
|
|
536
634
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
537
635
|
: undefined;
|
|
636
|
+
const disarmTimeout = () => {
|
|
637
|
+
if (!timeoutHandle)
|
|
638
|
+
return;
|
|
639
|
+
clearTimeout(timeoutHandle);
|
|
640
|
+
timeoutHandle = undefined;
|
|
641
|
+
};
|
|
538
642
|
let proxy;
|
|
539
643
|
try {
|
|
540
|
-
throwIfAmbientAborted(
|
|
644
|
+
throwIfAmbientAborted(options.signal);
|
|
541
645
|
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
542
646
|
// failures are branded as TransportErrors and count against the request
|
|
543
647
|
// deadline, exactly as an inline resolve would.
|
|
544
648
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
545
|
-
|
|
649
|
+
observeProxy?.(Boolean(proxy));
|
|
650
|
+
throwIfAmbientAborted(options.signal);
|
|
546
651
|
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
547
652
|
// tried rather than re-issuing the same request. Returning the sentinel
|
|
548
653
|
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
@@ -553,8 +658,12 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
553
658
|
}
|
|
554
659
|
dedupe.attempted.add(proxy);
|
|
555
660
|
}
|
|
661
|
+
// Resolve after the dedupe check so a skipped duplicate offset never burns
|
|
662
|
+
// a single-use proof, and with the serialized URL so URL-bound proofs
|
|
663
|
+
// match what is actually issued.
|
|
664
|
+
const requestHeaders = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }), signal);
|
|
556
665
|
const requestInit = {
|
|
557
|
-
headers:
|
|
666
|
+
headers: requestHeaders,
|
|
558
667
|
method,
|
|
559
668
|
...(proxy ? { proxy } : {}),
|
|
560
669
|
...(signal ? { signal } : {}),
|
|
@@ -563,6 +672,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
563
672
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
564
673
|
}
|
|
565
674
|
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
675
|
+
observeProxy?.(Boolean(proxy), response.status);
|
|
566
676
|
const headers = Object.fromEntries(response.headers.entries());
|
|
567
677
|
if (statusRetryCodes && response.status >= 400) {
|
|
568
678
|
await drainNativeResponseBody(response);
|
|
@@ -581,35 +691,45 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
581
691
|
status: response.status,
|
|
582
692
|
});
|
|
583
693
|
}
|
|
584
|
-
|
|
694
|
+
// `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
|
|
695
|
+
// is disarmed before the body is read: awaiting the buffered body inside
|
|
696
|
+
// this try brands a mid-body failure as a transport error, but must not
|
|
697
|
+
// extend the deadline over the download. Cancellation still applies —
|
|
698
|
+
// `signal` stays attached to the native response. The discarded non-2xx
|
|
699
|
+
// bodies drained above stay under the deadline on purpose: nothing else
|
|
700
|
+
// bounds them, and their content is never returned to the caller.
|
|
701
|
+
disarmTimeout();
|
|
702
|
+
return await toNativeHttpResponse(response);
|
|
585
703
|
}
|
|
586
704
|
catch (error) {
|
|
587
705
|
if (error instanceof SyntaxError) {
|
|
588
706
|
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
589
707
|
}
|
|
590
|
-
const transportError = redactSensitiveError(toHttpTransportError(error,
|
|
708
|
+
const transportError = redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
591
709
|
transportError.proxyUsed = Boolean(proxy);
|
|
592
710
|
throw transportError;
|
|
593
711
|
}
|
|
594
712
|
finally {
|
|
595
|
-
|
|
596
|
-
clearTimeout(timeoutHandle);
|
|
713
|
+
disarmTimeout();
|
|
597
714
|
}
|
|
598
715
|
}
|
|
599
|
-
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
|
|
716
|
+
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn, observeProxy) {
|
|
600
717
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
601
718
|
const { requestUrl } = serializedUrl;
|
|
602
719
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
603
|
-
const signal = mergeAbortSignals(
|
|
720
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
604
721
|
const timeoutHandle = options.timeout
|
|
605
722
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
606
723
|
: undefined;
|
|
607
724
|
try {
|
|
608
|
-
throwIfAmbientAborted(
|
|
725
|
+
throwIfAmbientAborted(options.signal);
|
|
609
726
|
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
610
|
-
|
|
727
|
+
observeProxy?.(Boolean(proxy));
|
|
728
|
+
throwIfAmbientAborted(options.signal);
|
|
729
|
+
// Streams never retry: a header factory is resolved once, as attempt 1.
|
|
730
|
+
const headers = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: 1, url: requestUrl, method }), signal);
|
|
611
731
|
const requestInit = {
|
|
612
|
-
headers
|
|
732
|
+
headers,
|
|
613
733
|
method,
|
|
614
734
|
...(proxy ? { proxy } : {}),
|
|
615
735
|
...(signal ? { signal } : {}),
|
|
@@ -625,15 +745,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
|
|
|
625
745
|
status: response.status,
|
|
626
746
|
});
|
|
627
747
|
}
|
|
628
|
-
//
|
|
629
|
-
// stays attached to
|
|
630
|
-
return toNativeHttpStreamResponse(response, serializedUrl,
|
|
748
|
+
// The stream timeout remains header-scoped; composed ambient and local
|
|
749
|
+
// cancellation stays attached to native fetch for the body's full lifetime.
|
|
750
|
+
return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
|
|
631
751
|
}
|
|
632
752
|
catch (error) {
|
|
633
753
|
if (error instanceof SyntaxError) {
|
|
634
754
|
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
635
755
|
}
|
|
636
|
-
throw redactSensitiveError(toHttpTransportError(error,
|
|
756
|
+
throw redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
637
757
|
}
|
|
638
758
|
finally {
|
|
639
759
|
if (timeoutHandle)
|
|
@@ -676,164 +796,198 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
676
796
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
677
797
|
}
|
|
678
798
|
})();
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
799
|
+
const requestStarted = performance.now();
|
|
800
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), retryOptions?.preset);
|
|
801
|
+
try {
|
|
802
|
+
return await runRequest();
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
telemetry.finish(performance.now() - requestStarted);
|
|
806
|
+
}
|
|
807
|
+
async function runRequest() {
|
|
808
|
+
const retryEnabled = Boolean(retryOptions &&
|
|
809
|
+
retryOptions.attempts > 1 &&
|
|
810
|
+
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
811
|
+
const statusRetryEnabled = Boolean(retryEnabled &&
|
|
812
|
+
explicitRetry &&
|
|
813
|
+
retryOptions &&
|
|
814
|
+
retryOptions.statusCodes.length > 0 &&
|
|
815
|
+
headersOptions.throwOnHttpError !== false);
|
|
816
|
+
const attemptOptions = statusRetryEnabled
|
|
817
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
818
|
+
: headersOptions;
|
|
819
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
820
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
821
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
822
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
823
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
824
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
825
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
826
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
827
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
828
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
829
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
830
|
+
// the full span.
|
|
831
|
+
const policyProxy = (() => {
|
|
832
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
833
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
834
|
+
})();
|
|
835
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
836
|
+
const transportAttemptCap = retryOptions
|
|
837
|
+
? resolvePolicyTransportAttemptCap({
|
|
838
|
+
policy: policyProxy,
|
|
839
|
+
usesPolicyAllocator,
|
|
840
|
+
retryAttempts: retryOptions.attempts,
|
|
841
|
+
explicitRetry,
|
|
842
|
+
method: methodName,
|
|
843
|
+
})
|
|
844
|
+
: 1;
|
|
845
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
846
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
847
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
848
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
849
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
850
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
851
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
852
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
853
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
854
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
855
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
856
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
857
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
858
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
859
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
860
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
709
861
|
policy: policyProxy,
|
|
710
862
|
usesPolicyAllocator,
|
|
711
|
-
retryAttempts: retryOptions.attempts,
|
|
712
863
|
explicitRetry,
|
|
713
864
|
method: methodName,
|
|
714
|
-
})
|
|
715
|
-
:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
let lastErrorCode;
|
|
756
|
-
let lastStatus;
|
|
757
|
-
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
758
|
-
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
759
|
-
// increment it). Retry summaries and the status-retry budget must reflect
|
|
760
|
-
// issued requests, not the raw offset, so they stay accurate when partial
|
|
761
|
-
// allocations skip offsets.
|
|
762
|
-
let issued = 0;
|
|
763
|
-
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
764
|
-
throwIfAmbientAborted(clientOptions.signal);
|
|
765
|
-
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
766
|
-
// so the catch counts a thrown *transport* failure once without
|
|
767
|
-
// double-counting a status outcome that already incremented before it
|
|
768
|
-
// re-threw as an upstream HTTP error.
|
|
769
|
-
let issuedThisAttempt = false;
|
|
770
|
-
try {
|
|
771
|
-
const outcome = await executeOnce(attempt - 1);
|
|
865
|
+
});
|
|
866
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
|
|
867
|
+
// Numbers the attempts a header factory sees: only requests that reached
|
|
868
|
+
// header resolution count, so a failed proxy allocation (which increments
|
|
869
|
+
// the telemetry `issued` count below) or a dedupe-skipped offset does not
|
|
870
|
+
// make the first request that is actually built report `attempt: 2`.
|
|
871
|
+
let headerAttempts = 0;
|
|
872
|
+
const nextHeaderAttempt = () => {
|
|
873
|
+
headerAttempts += 1;
|
|
874
|
+
return headerAttempts;
|
|
875
|
+
};
|
|
876
|
+
const executeOnce = async (proxyAttemptOffset = 0) => {
|
|
877
|
+
const started = performance.now();
|
|
878
|
+
let proxyUsed = false;
|
|
879
|
+
let observedStatus;
|
|
880
|
+
const record = (event) => {
|
|
881
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
882
|
+
};
|
|
883
|
+
try {
|
|
884
|
+
const outcome = await fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext, (used, status) => {
|
|
885
|
+
proxyUsed = used;
|
|
886
|
+
observedStatus = status;
|
|
887
|
+
}, nextHeaderAttempt);
|
|
888
|
+
if (!isDedupeSkipOutcome(outcome))
|
|
889
|
+
record({
|
|
890
|
+
status: outcome.status,
|
|
891
|
+
...(isHttpStatusOutcome(outcome)
|
|
892
|
+
? { e: "upstream_http_error", statusRetry: true }
|
|
893
|
+
: {}),
|
|
894
|
+
});
|
|
895
|
+
return outcome;
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
// Record before the retry loop can catch/rethrow a terminal status.
|
|
899
|
+
record(httpAttemptFailure(error, observedStatus));
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
if (!retryEnabled || !retryOptions) {
|
|
904
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
905
|
+
const outcome = await executeOnce();
|
|
772
906
|
if (isDedupeSkipOutcome(outcome)) {
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
907
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
908
|
+
// but keep the union total.
|
|
909
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
910
|
+
code: "retry_exhausted",
|
|
911
|
+
});
|
|
777
912
|
}
|
|
778
|
-
issued += 1;
|
|
779
|
-
issuedThisAttempt = true;
|
|
780
913
|
if (isHttpStatusOutcome(outcome)) {
|
|
781
|
-
lastStatus = outcome.status;
|
|
782
|
-
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
783
|
-
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
914
|
throw toUpstreamHttpError(outcome.status);
|
|
787
915
|
}
|
|
788
|
-
|
|
789
|
-
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
790
|
-
throw toUpstreamHttpError(response.status);
|
|
791
|
-
}
|
|
792
|
-
if (issued > 1) {
|
|
793
|
-
const summary = {
|
|
794
|
-
attempts: issued,
|
|
795
|
-
retries: issued - 1,
|
|
796
|
-
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
797
|
-
transport: "native",
|
|
798
|
-
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
799
|
-
...(lastStatus ? { lastStatus } : {}),
|
|
800
|
-
};
|
|
801
|
-
clientOptions.onRetrySummary?.(summary);
|
|
802
|
-
}
|
|
803
|
-
return response;
|
|
916
|
+
return outcome;
|
|
804
917
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
918
|
+
let lastError;
|
|
919
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
920
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
921
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
922
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
923
|
+
// allocations skip offsets.
|
|
924
|
+
let issued = 0;
|
|
925
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
926
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
927
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
928
|
+
// so the catch counts a thrown *transport* failure once without
|
|
929
|
+
// double-counting a status outcome that already incremented before it
|
|
930
|
+
// re-threw as an upstream HTTP error.
|
|
931
|
+
let issuedThisAttempt = false;
|
|
932
|
+
try {
|
|
933
|
+
const outcome = await executeOnce(attempt - 1);
|
|
934
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
935
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
936
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
937
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
808
940
|
issued += 1;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
941
|
+
issuedThisAttempt = true;
|
|
942
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
943
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
944
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), headersOptions.signal);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
throw toUpstreamHttpError(outcome.status);
|
|
948
|
+
}
|
|
949
|
+
const response = outcome;
|
|
950
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
951
|
+
throw toUpstreamHttpError(response.status);
|
|
952
|
+
}
|
|
953
|
+
if (issued > 1) {
|
|
954
|
+
const summary = telemetry.toTenantRetryPayload();
|
|
955
|
+
if (summary)
|
|
956
|
+
telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
|
|
957
|
+
}
|
|
958
|
+
return response;
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
962
|
+
if (!issuedThisAttempt)
|
|
963
|
+
issued += 1;
|
|
964
|
+
lastError = error;
|
|
965
|
+
const proxyUsed = Boolean(error.proxyUsed);
|
|
966
|
+
if (attempt < transportAttemptCap &&
|
|
967
|
+
shouldRetryProxyTransportAttempt({
|
|
968
|
+
error,
|
|
969
|
+
explicitRetry,
|
|
970
|
+
method: methodName,
|
|
971
|
+
options: retryOptions,
|
|
972
|
+
proxyUsed,
|
|
973
|
+
})) {
|
|
974
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), headersOptions.signal);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
throw error;
|
|
823
978
|
}
|
|
824
|
-
throw error;
|
|
825
979
|
}
|
|
980
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
981
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
982
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
983
|
+
// failure rather than a synthetic exhaustion error.
|
|
984
|
+
if (lastError !== undefined) {
|
|
985
|
+
throw lastError;
|
|
986
|
+
}
|
|
987
|
+
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
988
|
+
code: "retry_exhausted",
|
|
989
|
+
});
|
|
826
990
|
}
|
|
827
|
-
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
828
|
-
// e.g. the final offsets of a partial allocation all resolved to
|
|
829
|
-
// already-tried endpoints and were skipped. Surface the last real transport
|
|
830
|
-
// failure rather than a synthetic exhaustion error.
|
|
831
|
-
if (lastError !== undefined) {
|
|
832
|
-
throw lastError;
|
|
833
|
-
}
|
|
834
|
-
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
835
|
-
code: "retry_exhausted",
|
|
836
|
-
});
|
|
837
991
|
}
|
|
838
992
|
async function streamRequest(url, method, options = {}) {
|
|
839
993
|
const { headersOptions, methodName } = (() => {
|
|
@@ -855,7 +1009,30 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
855
1009
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
856
1010
|
}
|
|
857
1011
|
})();
|
|
858
|
-
|
|
1012
|
+
// Streams never retry. Observe the header fetch without consuming its body.
|
|
1013
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
|
|
1014
|
+
const started = performance.now();
|
|
1015
|
+
const alreadyAborted = headersOptions.signal?.aborted === true;
|
|
1016
|
+
let proxyUsed = false;
|
|
1017
|
+
const record = (event) => {
|
|
1018
|
+
if (alreadyAborted)
|
|
1019
|
+
return;
|
|
1020
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
1021
|
+
};
|
|
1022
|
+
try {
|
|
1023
|
+
const response = await fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce, (used) => {
|
|
1024
|
+
proxyUsed = used;
|
|
1025
|
+
});
|
|
1026
|
+
record({ status: response.status });
|
|
1027
|
+
return response;
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
record(httpAttemptFailure(error));
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
finally {
|
|
1034
|
+
telemetry.finish(performance.now() - started);
|
|
1035
|
+
}
|
|
859
1036
|
}
|
|
860
1037
|
return {
|
|
861
1038
|
request: async (url, options = {}) => request(url, options.method ?? "GET", options),
|
|
@@ -865,10 +1042,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
865
1042
|
delete: async (url, options) => request(url, "DELETE", options),
|
|
866
1043
|
stream: async (url, options = {}) => streamRequest(url, options.method ?? "GET", options),
|
|
867
1044
|
sse: async (url, options = {}) => {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1045
|
+
const callerHeaders = options.headers;
|
|
1046
|
+
const headers = typeof callerHeaders === "function"
|
|
1047
|
+
? async (attempt) => {
|
|
1048
|
+
// Pass a malformed result through unchanged so the factory
|
|
1049
|
+
// validation rejects it instead of spreading it into `{}`.
|
|
1050
|
+
const minted = await callerHeaders(attempt);
|
|
1051
|
+
return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
|
|
1052
|
+
}
|
|
1053
|
+
: { Accept: "text/event-stream", ...callerHeaders };
|
|
872
1054
|
const response = await streamRequest(url, options.method ?? "GET", {
|
|
873
1055
|
...options,
|
|
874
1056
|
headers,
|