@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/src/runtime/http.ts
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
|
|
8
8
|
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
9
9
|
import type {
|
|
10
|
+
HttpAttemptContext,
|
|
10
11
|
HttpClient,
|
|
12
|
+
HttpHeadersFactory,
|
|
11
13
|
HttpMethod,
|
|
12
14
|
HttpRedirectPolicy,
|
|
13
15
|
HttpResponse,
|
|
@@ -29,6 +31,11 @@ import {
|
|
|
29
31
|
shouldRetryProxyTransportAttempt,
|
|
30
32
|
validateUnsafeProxyTransportRetryMethods,
|
|
31
33
|
} from "./proxy-retry-policy.js";
|
|
34
|
+
import {
|
|
35
|
+
httpTelemetryErrorCode,
|
|
36
|
+
type HttpTelemetrySink,
|
|
37
|
+
type HttpAttemptTelemetryEvent,
|
|
38
|
+
} from "./http-telemetry.js";
|
|
32
39
|
import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
|
|
33
40
|
import {
|
|
34
41
|
normalizeHttpRequestBody,
|
|
@@ -38,12 +45,70 @@ import {
|
|
|
38
45
|
serializeRequestUrl,
|
|
39
46
|
} from "./request-options.js";
|
|
40
47
|
|
|
48
|
+
import { startHttpTelemetry } from "./http-telemetry-guard.js";
|
|
49
|
+
import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
|
|
50
|
+
|
|
41
51
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
42
52
|
|
|
53
|
+
function httpAttemptFailure(
|
|
54
|
+
error: unknown,
|
|
55
|
+
status?: number,
|
|
56
|
+
): Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed"> {
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
|
|
60
|
+
status: proxyTransportRetryErrorStatus(error) ?? status,
|
|
61
|
+
...(error instanceof Error
|
|
62
|
+
? {
|
|
63
|
+
diagnostics: {
|
|
64
|
+
name: error.name,
|
|
65
|
+
message: error.message,
|
|
66
|
+
...(error.cause instanceof Error
|
|
67
|
+
? { cause: { name: error.cause.name, message: error.cause.message } }
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return { e: "other" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
export type HttpClientOptions = ProxyResolutionOptions & {
|
|
44
79
|
warn?: (message: string) => void;
|
|
45
80
|
userAgent?: string;
|
|
46
81
|
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
|
|
84
|
+
* and observed proxy use. Observer failures never change transport behavior.
|
|
85
|
+
* All hooks and their property reads are guarded, including `onRetrySummary`
|
|
86
|
+
* and the failure reporter. Invalid asynchronous results are ignored and never
|
|
87
|
+
* awaited. Observer failures set the log-only `telemetryFailed` marker; the
|
|
88
|
+
* marker and redacted error diagnostics are excluded from headers and tenant
|
|
89
|
+
* metadata. Tenant `meta.retry` retains its existing response shape.
|
|
90
|
+
*
|
|
91
|
+
* After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
|
|
92
|
+
* with the request collector. The adapter preserves arguments, direct-call,
|
|
93
|
+
* explicit `.call`/`.apply`, and unbound receivers, plus the original returned
|
|
94
|
+
* promise, response, stream, and error. Streams and SSE are observed at headers
|
|
95
|
+
* without consuming their bodies. SDK clients report every observed attempt
|
|
96
|
+
* and keep any observer configured here; routing is scoped to each call.
|
|
97
|
+
*
|
|
98
|
+
* Binding an existing adapter reuses its identity and replaces its collector:
|
|
99
|
+
* the latest bind receives subsequent calls, the earlier collector receives no
|
|
100
|
+
* further calls, and in-flight calls keep the collector captured at invocation.
|
|
101
|
+
* An opaque host invocation is one observed attempt with zero observed retries;
|
|
102
|
+
* its undisclosed retries and proxy settings cannot be inferred, and tenant
|
|
103
|
+
* `meta.retry` is omitted. Status, duration, and error class remain available
|
|
104
|
+
* in logs and telemetry headers.
|
|
105
|
+
*
|
|
106
|
+
* Non-object bindings and objects without a callable `request`, `get`, `post`,
|
|
107
|
+
* `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
|
|
108
|
+
* telemetry sibling and one process-wide warning for this reason:
|
|
109
|
+
* `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
|
|
110
|
+
*/
|
|
111
|
+
httpTelemetry?: HttpTelemetrySink;
|
|
47
112
|
signal?: AbortSignal;
|
|
48
113
|
};
|
|
49
114
|
|
|
@@ -131,26 +196,108 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
131
196
|
return Object.keys(headers).some((key) => key.toLowerCase() === needle);
|
|
132
197
|
}
|
|
133
198
|
|
|
134
|
-
function
|
|
135
|
-
|
|
199
|
+
function withClientHeaderDefaults(
|
|
200
|
+
headers: Record<string, string> | undefined,
|
|
136
201
|
clientOptions: HttpClientOptions,
|
|
137
202
|
body: unknown,
|
|
138
|
-
):
|
|
139
|
-
const
|
|
203
|
+
): Record<string, string> {
|
|
204
|
+
const merged: Record<string, string> = {
|
|
140
205
|
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
141
|
-
...
|
|
206
|
+
...headers,
|
|
142
207
|
};
|
|
143
208
|
|
|
144
|
-
if (body !== undefined && !hasHeader(
|
|
145
|
-
|
|
209
|
+
if (body !== undefined && !hasHeader(merged, "Content-Type")) {
|
|
210
|
+
merged["Content-Type"] = "application/json";
|
|
146
211
|
}
|
|
147
212
|
|
|
213
|
+
return merged;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function withClientHeaders(
|
|
217
|
+
options: RequestOptions | undefined,
|
|
218
|
+
clientOptions: HttpClientOptions,
|
|
219
|
+
body: unknown,
|
|
220
|
+
): RequestOptions {
|
|
221
|
+
// The ambient client signal is merged into the per-request one here so every
|
|
222
|
+
// downstream read (retry loop, sleep, body consumption) sees a single signal.
|
|
223
|
+
// The header-factory branch needs it too, otherwise factory-header requests
|
|
224
|
+
// would lose ambient cancellation entirely.
|
|
225
|
+
const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
|
|
226
|
+
// A header factory is resolved per issued attempt (resolveAttemptHeaders);
|
|
227
|
+
// the client defaults are applied to its output at that point.
|
|
228
|
+
if (typeof options?.headers === "function") return { ...options, signal };
|
|
148
229
|
return {
|
|
149
230
|
...options,
|
|
150
|
-
headers,
|
|
231
|
+
headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
|
|
232
|
+
signal,
|
|
151
233
|
};
|
|
152
234
|
}
|
|
153
235
|
|
|
236
|
+
function headerFactoryFailure(message: string, cause?: unknown): TransportError {
|
|
237
|
+
// A dedicated code keeps a broken proof minter out of the retry predicate:
|
|
238
|
+
// toHttpTransportError would otherwise brand a plain throw as
|
|
239
|
+
// transport_network_error, which the default retry policy re-issues.
|
|
240
|
+
// retryable:false is explicit so the served error envelope does not
|
|
241
|
+
// advertise a deterministic signing failure as retryable, and
|
|
242
|
+
// category:provider_error keeps a provider-local minting bug out of the
|
|
243
|
+
// upstream-failure signal (status 0 would otherwise fall through the
|
|
244
|
+
// observability ladder to upstream_http / source upstream_failure).
|
|
245
|
+
return new TransportError(message, {
|
|
246
|
+
code: "http_header_factory_failed",
|
|
247
|
+
status: 0,
|
|
248
|
+
retryable: false,
|
|
249
|
+
category: "provider_error",
|
|
250
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isHeaderRecord(value: unknown): value is Record<string, string> {
|
|
255
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Settle `promise`, or reject with the signal's reason as soon as it aborts, so
|
|
259
|
+
// a pending header factory cannot outlive the request deadline or an ambient
|
|
260
|
+
// cancellation (native fetch has not started yet, so nothing else observes them).
|
|
261
|
+
function settleBeforeAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
262
|
+
if (!signal) return promise;
|
|
263
|
+
return new Promise<T>((resolve, reject) => {
|
|
264
|
+
const onAbort = () => reject(signal.reason);
|
|
265
|
+
if (signal.aborted) onAbort();
|
|
266
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
267
|
+
// Always attach handlers: a late rejection after the abort must settle
|
|
268
|
+
// here (a no-op on the already-rejected promise), never go unhandled.
|
|
269
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function resolveAttemptHeaders(
|
|
274
|
+
options: RequestOptions & { body?: unknown },
|
|
275
|
+
clientOptions: HttpClientOptions,
|
|
276
|
+
attempt: () => HttpAttemptContext,
|
|
277
|
+
signal: AbortSignal | undefined,
|
|
278
|
+
): Promise<Record<string, string> | undefined> {
|
|
279
|
+
const { headers } = options;
|
|
280
|
+
if (typeof headers !== "function") return headers;
|
|
281
|
+
// The per-call timeout may have fired during proxy resolution: do not start
|
|
282
|
+
// the factory at all once the request is already aborted.
|
|
283
|
+
if (signal?.aborted) throw signal.reason;
|
|
284
|
+
let minted: unknown;
|
|
285
|
+
try {
|
|
286
|
+
minted = await settleBeforeAbort(
|
|
287
|
+
Promise.resolve().then(() => headers(attempt())),
|
|
288
|
+
signal,
|
|
289
|
+
);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// An abort is classified by toHttpTransportError (cancelled / timed out).
|
|
292
|
+
if (signal?.aborted) throw error;
|
|
293
|
+
throw headerFactoryFailure("Request header factory failed", error);
|
|
294
|
+
}
|
|
295
|
+
if (!isHeaderRecord(minted)) {
|
|
296
|
+
throw headerFactoryFailure("Request header factory must return a header record");
|
|
297
|
+
}
|
|
298
|
+
return withClientHeaderDefaults(minted, clientOptions, options.body);
|
|
299
|
+
}
|
|
300
|
+
|
|
154
301
|
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
155
302
|
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
156
303
|
|
|
@@ -406,7 +553,7 @@ function toNativeHttpStreamResponse(
|
|
|
406
553
|
};
|
|
407
554
|
}
|
|
408
555
|
|
|
409
|
-
function normalizeHttpMethod(method: string): HttpMethod {
|
|
556
|
+
function normalizeHttpMethod(method: string): Uppercase<HttpMethod> {
|
|
410
557
|
switch (method.toUpperCase()) {
|
|
411
558
|
case "HEAD":
|
|
412
559
|
return "HEAD";
|
|
@@ -615,7 +762,7 @@ async function fetchWithHttpRedirectPolicy(
|
|
|
615
762
|
return `Redirect loop refused target ${target}`;
|
|
616
763
|
case "missing_location":
|
|
617
764
|
return `Redirect response from ${redirectDiagnosticTarget(currentUrl)} is missing Location`;
|
|
618
|
-
|
|
765
|
+
}
|
|
619
766
|
})();
|
|
620
767
|
throw new HttpRedirectError(message, {
|
|
621
768
|
reason: decision.reason,
|
|
@@ -702,30 +849,38 @@ function serializeHttpRequestUrl(
|
|
|
702
849
|
async function fetchNativeHttp(
|
|
703
850
|
baseUrl: string | undefined,
|
|
704
851
|
url: string,
|
|
705
|
-
method: HttpMethod
|
|
852
|
+
method: Uppercase<HttpMethod>,
|
|
706
853
|
options: RequestOptions & { body?: unknown },
|
|
707
854
|
clientOptions: HttpClientOptions,
|
|
708
855
|
warn: (message: string) => void,
|
|
709
856
|
statusRetryCodes?: readonly number[],
|
|
710
857
|
proxyAttemptOffset = 0,
|
|
711
858
|
dedupe?: { attempted: Set<string> },
|
|
859
|
+
observeProxy?: (used: boolean, status?: number) => void,
|
|
860
|
+
nextHeaderAttempt: () => number = () => 1,
|
|
712
861
|
): Promise<NativeHttpAttemptOutcome> {
|
|
713
862
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
714
863
|
const { requestUrl } = serializedUrl;
|
|
715
864
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
716
|
-
const signal = mergeAbortSignals(
|
|
717
|
-
|
|
865
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
866
|
+
let timeoutHandle = options.timeout
|
|
718
867
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
719
868
|
: undefined;
|
|
869
|
+
const disarmTimeout = () => {
|
|
870
|
+
if (!timeoutHandle) return;
|
|
871
|
+
clearTimeout(timeoutHandle);
|
|
872
|
+
timeoutHandle = undefined;
|
|
873
|
+
};
|
|
720
874
|
|
|
721
875
|
let proxy: string | undefined;
|
|
722
876
|
try {
|
|
723
|
-
throwIfAmbientAborted(
|
|
877
|
+
throwIfAmbientAborted(options.signal);
|
|
724
878
|
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
725
879
|
// failures are branded as TransportErrors and count against the request
|
|
726
880
|
// deadline, exactly as an inline resolve would.
|
|
727
881
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
728
|
-
|
|
882
|
+
observeProxy?.(Boolean(proxy));
|
|
883
|
+
throwIfAmbientAborted(options.signal);
|
|
729
884
|
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
730
885
|
// tried rather than re-issuing the same request. Returning the sentinel
|
|
731
886
|
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
@@ -736,8 +891,17 @@ async function fetchNativeHttp(
|
|
|
736
891
|
}
|
|
737
892
|
dedupe.attempted.add(proxy);
|
|
738
893
|
}
|
|
894
|
+
// Resolve after the dedupe check so a skipped duplicate offset never burns
|
|
895
|
+
// a single-use proof, and with the serialized URL so URL-bound proofs
|
|
896
|
+
// match what is actually issued.
|
|
897
|
+
const requestHeaders = await resolveAttemptHeaders(
|
|
898
|
+
options,
|
|
899
|
+
clientOptions,
|
|
900
|
+
() => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }),
|
|
901
|
+
signal,
|
|
902
|
+
);
|
|
739
903
|
const requestInit: NativeFetchInit = {
|
|
740
|
-
headers:
|
|
904
|
+
headers: requestHeaders,
|
|
741
905
|
method,
|
|
742
906
|
...(proxy ? { proxy } : {}),
|
|
743
907
|
...(signal ? { signal } : {}),
|
|
@@ -750,6 +914,7 @@ async function fetchNativeHttp(
|
|
|
750
914
|
requestInit,
|
|
751
915
|
options.redirectPolicy,
|
|
752
916
|
);
|
|
917
|
+
observeProxy?.(Boolean(proxy), response.status);
|
|
753
918
|
const headers = Object.fromEntries(response.headers.entries());
|
|
754
919
|
|
|
755
920
|
if (statusRetryCodes && response.status >= 400) {
|
|
@@ -771,7 +936,15 @@ async function fetchNativeHttp(
|
|
|
771
936
|
});
|
|
772
937
|
}
|
|
773
938
|
|
|
774
|
-
|
|
939
|
+
// `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
|
|
940
|
+
// is disarmed before the body is read: awaiting the buffered body inside
|
|
941
|
+
// this try brands a mid-body failure as a transport error, but must not
|
|
942
|
+
// extend the deadline over the download. Cancellation still applies —
|
|
943
|
+
// `signal` stays attached to the native response. The discarded non-2xx
|
|
944
|
+
// bodies drained above stay under the deadline on purpose: nothing else
|
|
945
|
+
// bounds them, and their content is never returned to the caller.
|
|
946
|
+
disarmTimeout();
|
|
947
|
+
return await toNativeHttpResponse(response);
|
|
775
948
|
} catch (error) {
|
|
776
949
|
if (error instanceof SyntaxError) {
|
|
777
950
|
throw redactSensitiveError(
|
|
@@ -782,7 +955,7 @@ async function fetchNativeHttp(
|
|
|
782
955
|
);
|
|
783
956
|
}
|
|
784
957
|
const transportError: NativeHttpAttemptError = redactSensitiveError(
|
|
785
|
-
toHttpTransportError(error,
|
|
958
|
+
toHttpTransportError(error, options.signal, controller?.signal),
|
|
786
959
|
serializedUrl.sensitiveValues,
|
|
787
960
|
serializedUrl.requestUrl,
|
|
788
961
|
serializedUrl.redactedUrl,
|
|
@@ -790,32 +963,41 @@ async function fetchNativeHttp(
|
|
|
790
963
|
transportError.proxyUsed = Boolean(proxy);
|
|
791
964
|
throw transportError;
|
|
792
965
|
} finally {
|
|
793
|
-
|
|
966
|
+
disarmTimeout();
|
|
794
967
|
}
|
|
795
968
|
}
|
|
796
969
|
|
|
797
970
|
async function fetchNativeHttpStream(
|
|
798
971
|
baseUrl: string | undefined,
|
|
799
972
|
url: string,
|
|
800
|
-
method: HttpMethod
|
|
973
|
+
method: Uppercase<HttpMethod>,
|
|
801
974
|
options: RequestOptions & { body?: unknown },
|
|
802
975
|
clientOptions: HttpClientOptions,
|
|
803
976
|
warn: (message: string) => void,
|
|
977
|
+
observeProxy?: (used: boolean) => void,
|
|
804
978
|
): Promise<HttpStreamResponse> {
|
|
805
979
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
806
980
|
const { requestUrl } = serializedUrl;
|
|
807
981
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
808
|
-
const signal = mergeAbortSignals(
|
|
982
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
809
983
|
const timeoutHandle = options.timeout
|
|
810
984
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
811
985
|
: undefined;
|
|
812
986
|
|
|
813
987
|
try {
|
|
814
|
-
throwIfAmbientAborted(
|
|
988
|
+
throwIfAmbientAborted(options.signal);
|
|
815
989
|
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
816
|
-
|
|
990
|
+
observeProxy?.(Boolean(proxy));
|
|
991
|
+
throwIfAmbientAborted(options.signal);
|
|
992
|
+
// Streams never retry: a header factory is resolved once, as attempt 1.
|
|
993
|
+
const headers = await resolveAttemptHeaders(
|
|
994
|
+
options,
|
|
995
|
+
clientOptions,
|
|
996
|
+
() => ({ attempt: 1, url: requestUrl, method }),
|
|
997
|
+
signal,
|
|
998
|
+
);
|
|
817
999
|
const requestInit: NativeFetchInit = {
|
|
818
|
-
headers
|
|
1000
|
+
headers,
|
|
819
1001
|
method,
|
|
820
1002
|
...(proxy ? { proxy } : {}),
|
|
821
1003
|
...(signal ? { signal } : {}),
|
|
@@ -837,9 +1019,9 @@ async function fetchNativeHttpStream(
|
|
|
837
1019
|
});
|
|
838
1020
|
}
|
|
839
1021
|
|
|
840
|
-
//
|
|
841
|
-
// stays attached to
|
|
842
|
-
return toNativeHttpStreamResponse(response, serializedUrl,
|
|
1022
|
+
// The stream timeout remains header-scoped; composed ambient and local
|
|
1023
|
+
// cancellation stays attached to native fetch for the body's full lifetime.
|
|
1024
|
+
return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
|
|
843
1025
|
} catch (error) {
|
|
844
1026
|
if (error instanceof SyntaxError) {
|
|
845
1027
|
throw redactSensitiveError(
|
|
@@ -850,7 +1032,7 @@ async function fetchNativeHttpStream(
|
|
|
850
1032
|
);
|
|
851
1033
|
}
|
|
852
1034
|
throw redactSensitiveError(
|
|
853
|
-
toHttpTransportError(error,
|
|
1035
|
+
toHttpTransportError(error, options.signal, controller?.signal),
|
|
854
1036
|
serializedUrl.sensitiveValues,
|
|
855
1037
|
serializedUrl.requestUrl,
|
|
856
1038
|
serializedUrl.redactedUrl,
|
|
@@ -906,193 +1088,230 @@ export function createHttpClient(
|
|
|
906
1088
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
907
1089
|
}
|
|
908
1090
|
})();
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1091
|
+
const requestStarted = performance.now();
|
|
1092
|
+
const telemetry = startHttpTelemetry(
|
|
1093
|
+
scopedHttpTelemetry(clientOptions.httpTelemetry),
|
|
1094
|
+
retryOptions?.preset,
|
|
913
1095
|
);
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
headersOptions.throwOnHttpError !== false,
|
|
920
|
-
);
|
|
921
|
-
const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
|
|
922
|
-
? { ...headersOptions, throwOnHttpError: false }
|
|
923
|
-
: headersOptions;
|
|
924
|
-
|
|
925
|
-
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
926
|
-
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
927
|
-
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
928
|
-
// spans), so a transport failure should advance to the next endpoint —
|
|
929
|
-
// potentially crossing into the fallback vendor — rather than stopping at
|
|
930
|
-
// the per-endpoint retry budget and stranding the request on the primary
|
|
931
|
-
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
932
|
-
// only for implicit, safe-method allocator requests; explicit retry
|
|
933
|
-
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
934
|
-
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
935
|
-
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
936
|
-
// the full span.
|
|
937
|
-
const policyProxy: ProviderProxyPolicy | undefined = (() => {
|
|
938
|
-
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
939
|
-
return policy && typeof policy === "object" ? policy : undefined;
|
|
940
|
-
})();
|
|
941
|
-
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
942
|
-
const transportAttemptCap = retryOptions
|
|
943
|
-
? resolvePolicyTransportAttemptCap({
|
|
944
|
-
policy: policyProxy,
|
|
945
|
-
usesPolicyAllocator,
|
|
946
|
-
retryAttempts: retryOptions.attempts,
|
|
947
|
-
explicitRetry,
|
|
948
|
-
method: methodName,
|
|
949
|
-
})
|
|
950
|
-
: 1;
|
|
951
|
-
|
|
952
|
-
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
953
|
-
// attempts rotate the flat offset across the concatenated vendor pool
|
|
954
|
-
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
955
|
-
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
956
|
-
// the offset reaches the next vendor. Rather than re-hammering an
|
|
957
|
-
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
958
|
-
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
959
|
-
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
960
|
-
// vendor's pool span instead of stalling on the primary vendor.
|
|
961
|
-
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
962
|
-
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
963
|
-
// an explicit retry policy: there the caller's `attempts` count is the
|
|
964
|
-
// contract and each attempt must issue against whatever endpoint it resolves
|
|
965
|
-
// — even a repeat — instead of being silently skipped (which would collapse
|
|
966
|
-
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
967
|
-
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
968
|
-
policy: policyProxy,
|
|
969
|
-
usesPolicyAllocator,
|
|
970
|
-
explicitRetry,
|
|
971
|
-
method: methodName,
|
|
972
|
-
});
|
|
973
|
-
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
|
|
1096
|
+
try {
|
|
1097
|
+
return await runRequest();
|
|
1098
|
+
} finally {
|
|
1099
|
+
telemetry.finish(performance.now() - requestStarted);
|
|
1100
|
+
}
|
|
974
1101
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
attemptOptions,
|
|
981
|
-
clientOptions,
|
|
982
|
-
warnOnce,
|
|
983
|
-
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
984
|
-
proxyAttemptOffset,
|
|
985
|
-
dedupeContext,
|
|
1102
|
+
async function runRequest(): Promise<HttpResponse> {
|
|
1103
|
+
const retryEnabled = Boolean(
|
|
1104
|
+
retryOptions &&
|
|
1105
|
+
retryOptions.attempts > 1 &&
|
|
1106
|
+
isProxyTransportRetryMethod(methodName, retryOptions),
|
|
986
1107
|
);
|
|
1108
|
+
const statusRetryEnabled = Boolean(
|
|
1109
|
+
retryEnabled &&
|
|
1110
|
+
explicitRetry &&
|
|
1111
|
+
retryOptions &&
|
|
1112
|
+
retryOptions.statusCodes.length > 0 &&
|
|
1113
|
+
headersOptions.throwOnHttpError !== false,
|
|
1114
|
+
);
|
|
1115
|
+
const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
|
|
1116
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
1117
|
+
: headersOptions;
|
|
1118
|
+
|
|
1119
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
1120
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
1121
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
1122
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
1123
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
1124
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
1125
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
1126
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
1127
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
1128
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
1129
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
1130
|
+
// the full span.
|
|
1131
|
+
const policyProxy: ProviderProxyPolicy | undefined = (() => {
|
|
1132
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
1133
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
1134
|
+
})();
|
|
1135
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
1136
|
+
const transportAttemptCap = retryOptions
|
|
1137
|
+
? resolvePolicyTransportAttemptCap({
|
|
1138
|
+
policy: policyProxy,
|
|
1139
|
+
usesPolicyAllocator,
|
|
1140
|
+
retryAttempts: retryOptions.attempts,
|
|
1141
|
+
explicitRetry,
|
|
1142
|
+
method: methodName,
|
|
1143
|
+
})
|
|
1144
|
+
: 1;
|
|
1145
|
+
|
|
1146
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
1147
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
1148
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
1149
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
1150
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
1151
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
1152
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
1153
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
1154
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
1155
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
1156
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
1157
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
1158
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
1159
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
1160
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
1161
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
1162
|
+
policy: policyProxy,
|
|
1163
|
+
usesPolicyAllocator,
|
|
1164
|
+
explicitRetry,
|
|
1165
|
+
method: methodName,
|
|
1166
|
+
});
|
|
1167
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
|
|
1168
|
+
|
|
1169
|
+
// Numbers the attempts a header factory sees: only requests that reached
|
|
1170
|
+
// header resolution count, so a failed proxy allocation (which increments
|
|
1171
|
+
// the telemetry `issued` count below) or a dedupe-skipped offset does not
|
|
1172
|
+
// make the first request that is actually built report `attempt: 2`.
|
|
1173
|
+
let headerAttempts = 0;
|
|
1174
|
+
const nextHeaderAttempt = () => {
|
|
1175
|
+
headerAttempts += 1;
|
|
1176
|
+
return headerAttempts;
|
|
1177
|
+
};
|
|
987
1178
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1179
|
+
const executeOnce = async (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> => {
|
|
1180
|
+
const started = performance.now();
|
|
1181
|
+
let proxyUsed = false;
|
|
1182
|
+
let observedStatus: number | undefined;
|
|
1183
|
+
const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
|
|
1184
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
1185
|
+
};
|
|
1186
|
+
try {
|
|
1187
|
+
const outcome = await fetchNativeHttp(
|
|
1188
|
+
baseUrl,
|
|
1189
|
+
url,
|
|
1190
|
+
methodName,
|
|
1191
|
+
attemptOptions,
|
|
1192
|
+
clientOptions,
|
|
1193
|
+
warnOnce,
|
|
1194
|
+
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
1195
|
+
proxyAttemptOffset,
|
|
1196
|
+
dedupeContext,
|
|
1197
|
+
(used, status) => {
|
|
1198
|
+
proxyUsed = used;
|
|
1199
|
+
observedStatus = status;
|
|
1200
|
+
},
|
|
1201
|
+
nextHeaderAttempt,
|
|
1202
|
+
);
|
|
1203
|
+
if (!isDedupeSkipOutcome(outcome))
|
|
1204
|
+
record({
|
|
1205
|
+
status: outcome.status,
|
|
1206
|
+
...(isHttpStatusOutcome(outcome)
|
|
1207
|
+
? { e: "upstream_http_error", statusRetry: true }
|
|
1208
|
+
: {}),
|
|
1209
|
+
});
|
|
1210
|
+
return outcome;
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
// Record before the retry loop can catch/rethrow a terminal status.
|
|
1213
|
+
record(httpAttemptFailure(error, observedStatus));
|
|
1214
|
+
throw error;
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1003
1217
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
1008
|
-
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
1009
|
-
// increment it). Retry summaries and the status-retry budget must reflect
|
|
1010
|
-
// issued requests, not the raw offset, so they stay accurate when partial
|
|
1011
|
-
// allocations skip offsets.
|
|
1012
|
-
let issued = 0;
|
|
1013
|
-
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
1014
|
-
throwIfAmbientAborted(clientOptions.signal);
|
|
1015
|
-
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
1016
|
-
// so the catch counts a thrown *transport* failure once without
|
|
1017
|
-
// double-counting a status outcome that already incremented before it
|
|
1018
|
-
// re-threw as an upstream HTTP error.
|
|
1019
|
-
let issuedThisAttempt = false;
|
|
1020
|
-
try {
|
|
1021
|
-
const outcome = await executeOnce(attempt - 1);
|
|
1218
|
+
if (!retryEnabled || !retryOptions) {
|
|
1219
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1220
|
+
const outcome = await executeOnce();
|
|
1022
1221
|
if (isDedupeSkipOutcome(outcome)) {
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1222
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
1223
|
+
// but keep the union total.
|
|
1224
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
1225
|
+
code: "retry_exhausted",
|
|
1226
|
+
});
|
|
1027
1227
|
}
|
|
1028
|
-
issued += 1;
|
|
1029
|
-
issuedThisAttempt = true;
|
|
1030
1228
|
if (isHttpStatusOutcome(outcome)) {
|
|
1031
|
-
lastStatus = outcome.status;
|
|
1032
|
-
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
1033
|
-
await sleep(
|
|
1034
|
-
computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
1035
|
-
clientOptions.signal,
|
|
1036
|
-
);
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
1229
|
throw toUpstreamHttpError(outcome.status);
|
|
1040
1230
|
}
|
|
1231
|
+
return outcome;
|
|
1232
|
+
}
|
|
1041
1233
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1234
|
+
let lastError: unknown;
|
|
1235
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
1236
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
1237
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
1238
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
1239
|
+
// allocations skip offsets.
|
|
1240
|
+
let issued = 0;
|
|
1241
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
1242
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1243
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
1244
|
+
// so the catch counts a thrown *transport* failure once without
|
|
1245
|
+
// double-counting a status outcome that already incremented before it
|
|
1246
|
+
// re-threw as an upstream HTTP error.
|
|
1247
|
+
let issuedThisAttempt = false;
|
|
1248
|
+
try {
|
|
1249
|
+
const outcome = await executeOnce(attempt - 1);
|
|
1250
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
1251
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
1252
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
1253
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
issued += 1;
|
|
1257
|
+
issuedThisAttempt = true;
|
|
1258
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
1259
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
1260
|
+
await sleep(
|
|
1261
|
+
computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
1262
|
+
headersOptions.signal,
|
|
1263
|
+
);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
throw toUpstreamHttpError(outcome.status);
|
|
1267
|
+
}
|
|
1046
1268
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
);
|
|
1080
|
-
continue;
|
|
1269
|
+
const response = outcome;
|
|
1270
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
1271
|
+
throw toUpstreamHttpError(response.status);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (issued > 1) {
|
|
1275
|
+
const summary = telemetry.toTenantRetryPayload();
|
|
1276
|
+
if (summary) telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
|
|
1277
|
+
}
|
|
1278
|
+
return response;
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1281
|
+
if (!issuedThisAttempt) issued += 1;
|
|
1282
|
+
lastError = error;
|
|
1283
|
+
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
1284
|
+
if (
|
|
1285
|
+
attempt < transportAttemptCap &&
|
|
1286
|
+
shouldRetryProxyTransportAttempt({
|
|
1287
|
+
error,
|
|
1288
|
+
explicitRetry,
|
|
1289
|
+
method: methodName,
|
|
1290
|
+
options: retryOptions,
|
|
1291
|
+
proxyUsed,
|
|
1292
|
+
})
|
|
1293
|
+
) {
|
|
1294
|
+
await sleep(
|
|
1295
|
+
computeProxyTransportRetryDelayMs(retryOptions, attempt),
|
|
1296
|
+
headersOptions.signal,
|
|
1297
|
+
);
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
throw error;
|
|
1081
1301
|
}
|
|
1082
|
-
throw error;
|
|
1083
1302
|
}
|
|
1084
|
-
}
|
|
1085
1303
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1304
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
1305
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
1306
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
1307
|
+
// failure rather than a synthetic exhaustion error.
|
|
1308
|
+
if (lastError !== undefined) {
|
|
1309
|
+
throw lastError;
|
|
1310
|
+
}
|
|
1311
|
+
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
1312
|
+
code: "retry_exhausted",
|
|
1313
|
+
});
|
|
1092
1314
|
}
|
|
1093
|
-
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
1094
|
-
code: "retry_exhausted",
|
|
1095
|
-
});
|
|
1096
1315
|
}
|
|
1097
1316
|
|
|
1098
1317
|
async function streamRequest(
|
|
@@ -1121,7 +1340,35 @@ export function createHttpClient(
|
|
|
1121
1340
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
1122
1341
|
}
|
|
1123
1342
|
})();
|
|
1124
|
-
|
|
1343
|
+
// Streams never retry. Observe the header fetch without consuming its body.
|
|
1344
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
|
|
1345
|
+
const started = performance.now();
|
|
1346
|
+
const alreadyAborted = headersOptions.signal?.aborted === true;
|
|
1347
|
+
let proxyUsed = false;
|
|
1348
|
+
const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
|
|
1349
|
+
if (alreadyAborted) return;
|
|
1350
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
1351
|
+
};
|
|
1352
|
+
try {
|
|
1353
|
+
const response = await fetchNativeHttpStream(
|
|
1354
|
+
baseUrl,
|
|
1355
|
+
url,
|
|
1356
|
+
methodName,
|
|
1357
|
+
headersOptions,
|
|
1358
|
+
clientOptions,
|
|
1359
|
+
warnOnce,
|
|
1360
|
+
(used) => {
|
|
1361
|
+
proxyUsed = used;
|
|
1362
|
+
},
|
|
1363
|
+
);
|
|
1364
|
+
record({ status: response.status });
|
|
1365
|
+
return response;
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
record(httpAttemptFailure(error));
|
|
1368
|
+
throw error;
|
|
1369
|
+
} finally {
|
|
1370
|
+
telemetry.finish(performance.now() - started);
|
|
1371
|
+
}
|
|
1125
1372
|
}
|
|
1126
1373
|
|
|
1127
1374
|
return {
|
|
@@ -1137,10 +1384,16 @@ export function createHttpClient(
|
|
|
1137
1384
|
url,
|
|
1138
1385
|
options: RequestWithMethodOptions = {},
|
|
1139
1386
|
): Promise<AsyncIterable<SseMessage>> => {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1387
|
+
const callerHeaders = options.headers;
|
|
1388
|
+
const headers: Record<string, string> | HttpHeadersFactory =
|
|
1389
|
+
typeof callerHeaders === "function"
|
|
1390
|
+
? async (attempt) => {
|
|
1391
|
+
// Pass a malformed result through unchanged so the factory
|
|
1392
|
+
// validation rejects it instead of spreading it into `{}`.
|
|
1393
|
+
const minted = await callerHeaders(attempt);
|
|
1394
|
+
return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
|
|
1395
|
+
}
|
|
1396
|
+
: { Accept: "text/event-stream", ...callerHeaders };
|
|
1144
1397
|
const response = await streamRequest(url, options.method ?? "GET", {
|
|
1145
1398
|
...options,
|
|
1146
1399
|
headers,
|