@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.10
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 +106 -0
- package/CHANGELOG.md +37 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +510 -45
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +149 -8
- package/dist/config/loader.js +378 -89
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +8 -1
- package/dist/define.js +119 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +44 -42
- package/dist/index.js +39 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +144 -348
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +2 -2
- package/dist/runtime/stealth.js +108 -212
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +8 -3
- package/dist/server/serve.js +105 -59
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -7
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +1 -1
- package/dist/testing/run.js +7 -14
- package/dist/types.d.ts +87 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +9 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +247 -505
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +49 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +15 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +23 -20
- package/src/runtime/http.ts +195 -559
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +162 -377
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +12 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +172 -245
- package/src/server/types.ts +7 -19
- package/src/stealth/profiles.ts +10 -26
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +19 -67
- package/src/types.ts +88 -1
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/runtime/http.ts
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import type { ProxyResolutionOptions } from "../config/loader";
|
|
2
|
-
import { resolveProxyConfigAsync } from "../config/loader";
|
|
3
|
-
import { ProviderError, TransportError } from "../errors";
|
|
1
|
+
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../
|
|
3
|
+
policyRotatesTransportVendorChain,
|
|
4
|
+
resolvePolicyTransportAttemptCap,
|
|
5
|
+
resolveProxyConfigAsync,
|
|
6
|
+
} from "../config/loader.js";
|
|
7
|
+
import { ProviderError, TransportError } from "../errors.js";
|
|
8
|
+
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
10
9
|
import type {
|
|
11
10
|
HttpClient,
|
|
12
11
|
HttpMethod,
|
|
13
12
|
HttpResponse,
|
|
14
|
-
HttpRetryOptions,
|
|
15
13
|
HttpRetrySummary,
|
|
16
14
|
HttpStreamResponse,
|
|
15
|
+
ProviderProxyPolicy,
|
|
17
16
|
RequestOptions,
|
|
18
17
|
RequestWithMethodOptions,
|
|
19
18
|
SseMessage,
|
|
20
|
-
} from "../types";
|
|
19
|
+
} from "../types.js";
|
|
21
20
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
computeProxyAttemptIndex,
|
|
22
|
+
computeProxyTransportRetryDelayMs,
|
|
23
|
+
createDefaultProxyTransportRetryOptions,
|
|
24
|
+
isProxyTransportRetryMethod,
|
|
25
|
+
normalizeProxyTransportRetryOptions,
|
|
26
|
+
proxyTransportRetryErrorCode,
|
|
27
|
+
proxyTransportRetryErrorStatus,
|
|
28
|
+
shouldRetryProxyTransportAttempt,
|
|
29
|
+
validateUnsafeProxyTransportRetryMethods,
|
|
30
|
+
} from "./proxy-retry-policy.js";
|
|
31
|
+
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
|
|
29
32
|
|
|
30
33
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
31
34
|
|
|
@@ -35,24 +38,6 @@ export type HttpClientOptions = ProxyResolutionOptions & {
|
|
|
35
38
|
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
|
-
type NormalizedRetryOptions = Required<
|
|
39
|
-
Pick<
|
|
40
|
-
HttpRetryOptions,
|
|
41
|
-
| "attempts"
|
|
42
|
-
| "delayStrategy"
|
|
43
|
-
| "baseDelayMs"
|
|
44
|
-
| "maxDelayMs"
|
|
45
|
-
| "jitter"
|
|
46
|
-
| "retryAfter"
|
|
47
|
-
| "unsafeMethodPolicy"
|
|
48
|
-
>
|
|
49
|
-
> & {
|
|
50
|
-
preset?: HttpRetryPreset;
|
|
51
|
-
methods: readonly string[];
|
|
52
|
-
statusCodes: readonly number[];
|
|
53
|
-
errorCodes: readonly string[];
|
|
54
|
-
};
|
|
55
|
-
|
|
56
41
|
type HttpStatusOutcome = {
|
|
57
42
|
kind: "http-status";
|
|
58
43
|
status: number;
|
|
@@ -61,424 +46,26 @@ type HttpStatusOutcome = {
|
|
|
61
46
|
proxyUsed: boolean;
|
|
62
47
|
};
|
|
63
48
|
|
|
64
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Sentinel returned when a policy-allocator attempt resolved an endpoint that a
|
|
51
|
+
* prior attempt already tried (an under-filled pool repeats endpoints via the
|
|
52
|
+
* modulo mapping before the flat offset crosses into the next vendor). The retry
|
|
53
|
+
* loop advances to the next offset rather than re-issuing the request — but does
|
|
54
|
+
* NOT treat it as chain exhaustion, so the offset still walks into the fallback
|
|
55
|
+
* vendor's span.
|
|
56
|
+
*/
|
|
57
|
+
type NativeHttpSkipOutcome = { kind: "dedupe-skip" };
|
|
58
|
+
|
|
59
|
+
type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome | NativeHttpSkipOutcome;
|
|
65
60
|
|
|
66
61
|
type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
|
|
67
62
|
|
|
68
|
-
function isHttpStatusOutcome(
|
|
69
|
-
outcome: HttpResponse | HttpStatusOutcome,
|
|
70
|
-
): outcome is HttpStatusOutcome {
|
|
63
|
+
function isHttpStatusOutcome(outcome: NativeHttpAttemptOutcome): outcome is HttpStatusOutcome {
|
|
71
64
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"transport_network_error",
|
|
77
|
-
"transport_timeout",
|
|
78
|
-
] as const;
|
|
79
|
-
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
|
|
80
|
-
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503] as const;
|
|
81
|
-
const KNOWN_RETRY_METHODS = new Set([
|
|
82
|
-
"GET",
|
|
83
|
-
"HEAD",
|
|
84
|
-
"POST",
|
|
85
|
-
"PUT",
|
|
86
|
-
"DELETE",
|
|
87
|
-
"OPTIONS",
|
|
88
|
-
"TRACE",
|
|
89
|
-
"PATCH",
|
|
90
|
-
]);
|
|
91
|
-
const UNSAFE_RETRY_METHODS = new Set([
|
|
92
|
-
"POST",
|
|
93
|
-
"PUT",
|
|
94
|
-
"PATCH",
|
|
95
|
-
"DELETE",
|
|
96
|
-
"TRACE",
|
|
97
|
-
]);
|
|
98
|
-
const MAX_RETRY_ATTEMPTS = 8;
|
|
99
|
-
const MAX_RETRY_DELAY_MS = 30_000;
|
|
100
|
-
|
|
101
|
-
function hasOwnValue<T extends string>(
|
|
102
|
-
values: Record<string, T>,
|
|
103
|
-
value: unknown,
|
|
104
|
-
): value is T {
|
|
105
|
-
if (typeof value !== "string") return false;
|
|
106
|
-
return Object.values(values).some((candidate) => candidate === value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function createInvalidRetryPolicyError(message: string): ProviderError {
|
|
110
|
-
return new ProviderError(message, { code: "retry_invalid_policy" });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function createRetryOptions(preset: HttpRetryPreset): NormalizedRetryOptions {
|
|
114
|
-
switch (preset) {
|
|
115
|
-
case HttpRetryPreset.Off:
|
|
116
|
-
return {
|
|
117
|
-
preset,
|
|
118
|
-
attempts: 1,
|
|
119
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
120
|
-
statusCodes: [],
|
|
121
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
122
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
123
|
-
baseDelayMs: 100,
|
|
124
|
-
maxDelayMs: 1_000,
|
|
125
|
-
jitter: HttpRetryJitter.Full,
|
|
126
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
127
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
128
|
-
};
|
|
129
|
-
case HttpRetryPreset.SafeRead:
|
|
130
|
-
return {
|
|
131
|
-
preset,
|
|
132
|
-
attempts: 3,
|
|
133
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
134
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
135
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
136
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
137
|
-
baseDelayMs: 100,
|
|
138
|
-
maxDelayMs: 2_000,
|
|
139
|
-
jitter: HttpRetryJitter.Full,
|
|
140
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
141
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
142
|
-
};
|
|
143
|
-
case HttpRetryPreset.AggressiveRead:
|
|
144
|
-
return {
|
|
145
|
-
preset,
|
|
146
|
-
attempts: 4,
|
|
147
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
148
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
149
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
150
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
151
|
-
baseDelayMs: 150,
|
|
152
|
-
maxDelayMs: 5_000,
|
|
153
|
-
jitter: HttpRetryJitter.Full,
|
|
154
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
155
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
156
|
-
};
|
|
157
|
-
case HttpRetryPreset.RateLimitAware:
|
|
158
|
-
return {
|
|
159
|
-
preset,
|
|
160
|
-
attempts: 3,
|
|
161
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
162
|
-
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
163
|
-
errorCodes: ["transport_timeout"],
|
|
164
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
165
|
-
baseDelayMs: 250,
|
|
166
|
-
maxDelayMs: 5_000,
|
|
167
|
-
jitter: HttpRetryJitter.Equal,
|
|
168
|
-
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
169
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
170
|
-
};
|
|
171
|
-
case HttpRetryPreset.TransportTransient:
|
|
172
|
-
return {
|
|
173
|
-
preset,
|
|
174
|
-
attempts: 3,
|
|
175
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
176
|
-
statusCodes: [],
|
|
177
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
178
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
179
|
-
baseDelayMs: 100,
|
|
180
|
-
maxDelayMs: 1_000,
|
|
181
|
-
jitter: HttpRetryJitter.Full,
|
|
182
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
183
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function clampPositiveInteger(
|
|
190
|
-
value: number | undefined,
|
|
191
|
-
fallback: number,
|
|
192
|
-
max: number,
|
|
193
|
-
): number {
|
|
194
|
-
if (value === undefined) return fallback;
|
|
195
|
-
if (!Number.isFinite(value) || value < 1) return fallback;
|
|
196
|
-
return Math.min(Math.floor(value), max);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function clampDelay(value: number | undefined, fallback: number): number {
|
|
200
|
-
if (value === undefined) return fallback;
|
|
201
|
-
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
202
|
-
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function normalizeRetryOptions(
|
|
206
|
-
retry: RequestOptions["retry"],
|
|
207
|
-
): NormalizedRetryOptions | undefined {
|
|
208
|
-
if (retry === undefined || retry === false) {
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
if (retry === true) {
|
|
212
|
-
return createRetryOptions(HttpRetryPreset.TransportTransient);
|
|
213
|
-
}
|
|
214
|
-
if (typeof retry === "string") {
|
|
215
|
-
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
216
|
-
throw createInvalidRetryPolicyError(
|
|
217
|
-
`Unknown HTTP retry preset: ${retry}`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
return createRetryOptions(retry);
|
|
221
|
-
}
|
|
222
|
-
if (typeof retry !== "object" || retry === null) {
|
|
223
|
-
throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
|
|
224
|
-
}
|
|
225
|
-
if (Array.isArray(retry)) {
|
|
226
|
-
throw createInvalidRetryPolicyError(
|
|
227
|
-
"HTTP retry policy must be a plain object",
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
validateRetryOptionsShape(retry);
|
|
232
|
-
const base = createRetryOptions(
|
|
233
|
-
retry.preset ?? HttpRetryPreset.TransportTransient,
|
|
234
|
-
);
|
|
235
|
-
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
236
|
-
const normalized: NormalizedRetryOptions = {
|
|
237
|
-
preset: retry.preset ?? base.preset,
|
|
238
|
-
attempts: clampPositiveInteger(
|
|
239
|
-
retry.attempts,
|
|
240
|
-
base.attempts,
|
|
241
|
-
MAX_RETRY_ATTEMPTS,
|
|
242
|
-
),
|
|
243
|
-
methods:
|
|
244
|
-
retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
245
|
-
statusCodes:
|
|
246
|
-
retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
|
|
247
|
-
base.statusCodes,
|
|
248
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
249
|
-
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
250
|
-
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
251
|
-
maxDelayMs,
|
|
252
|
-
jitter: retry.jitter ?? base.jitter,
|
|
253
|
-
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
254
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
return normalized;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function validateRetryOptionsShape(retry: HttpRetryOptions): void {
|
|
261
|
-
if (
|
|
262
|
-
retry.preset !== undefined &&
|
|
263
|
-
!hasOwnValue(HttpRetryPreset, retry.preset)
|
|
264
|
-
) {
|
|
265
|
-
throw createInvalidRetryPolicyError(
|
|
266
|
-
`Unknown HTTP retry preset: ${String(retry.preset)}`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (
|
|
270
|
-
retry.delayStrategy !== undefined &&
|
|
271
|
-
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)
|
|
272
|
-
) {
|
|
273
|
-
throw createInvalidRetryPolicyError(
|
|
274
|
-
`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (
|
|
278
|
-
retry.jitter !== undefined &&
|
|
279
|
-
!hasOwnValue(HttpRetryJitter, retry.jitter)
|
|
280
|
-
) {
|
|
281
|
-
throw createInvalidRetryPolicyError(
|
|
282
|
-
`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
if (
|
|
286
|
-
retry.retryAfter !== undefined &&
|
|
287
|
-
!hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)
|
|
288
|
-
) {
|
|
289
|
-
throw createInvalidRetryPolicyError(
|
|
290
|
-
`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
if (
|
|
294
|
-
retry.unsafeMethodPolicy !== undefined &&
|
|
295
|
-
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)
|
|
296
|
-
) {
|
|
297
|
-
throw createInvalidRetryPolicyError(
|
|
298
|
-
`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
if (retry.methods !== undefined) {
|
|
302
|
-
if (!Array.isArray(retry.methods)) {
|
|
303
|
-
throw createInvalidRetryPolicyError(
|
|
304
|
-
"HTTP retry methods must be an array",
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
const nonStringMethods = retry.methods.filter(
|
|
308
|
-
(method) => typeof method !== "string",
|
|
309
|
-
);
|
|
310
|
-
if (nonStringMethods.length > 0) {
|
|
311
|
-
throw createInvalidRetryPolicyError(
|
|
312
|
-
"HTTP retry methods must contain only strings",
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
const unknownMethods = retry.methods
|
|
316
|
-
.map((method) => method.toUpperCase())
|
|
317
|
-
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
318
|
-
if (unknownMethods.length > 0) {
|
|
319
|
-
throw createInvalidRetryPolicyError(
|
|
320
|
-
`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (retry.statusCodes !== undefined) {
|
|
325
|
-
if (!Array.isArray(retry.statusCodes)) {
|
|
326
|
-
throw createInvalidRetryPolicyError(
|
|
327
|
-
"HTTP retry statusCodes must be an array",
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
const invalidStatusCodes = retry.statusCodes.filter(
|
|
331
|
-
(status) =>
|
|
332
|
-
!Number.isInteger(status) ||
|
|
333
|
-
Number(status) < 100 ||
|
|
334
|
-
Number(status) > 599,
|
|
335
|
-
);
|
|
336
|
-
if (invalidStatusCodes.length > 0) {
|
|
337
|
-
throw createInvalidRetryPolicyError(
|
|
338
|
-
"HTTP retry statusCodes must contain HTTP status integers in [100, 599]",
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (retry.errorCodes !== undefined) {
|
|
343
|
-
if (!Array.isArray(retry.errorCodes)) {
|
|
344
|
-
throw createInvalidRetryPolicyError(
|
|
345
|
-
"HTTP retry errorCodes must be an array",
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const nonStringErrorCodes = retry.errorCodes.filter(
|
|
349
|
-
(errorCode) => typeof errorCode !== "string",
|
|
350
|
-
);
|
|
351
|
-
if (nonStringErrorCodes.length > 0) {
|
|
352
|
-
throw createInvalidRetryPolicyError(
|
|
353
|
-
"HTTP retry errorCodes must contain only strings",
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (
|
|
358
|
-
retry.preset === HttpRetryPreset.Off &&
|
|
359
|
-
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
360
|
-
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))
|
|
361
|
-
) {
|
|
362
|
-
throw createInvalidRetryPolicyError(
|
|
363
|
-
"HTTP retry preset off cannot be combined with retry-enabling overrides",
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function validateUnsafeRetryMethods(options: NormalizedRetryOptions): void {
|
|
369
|
-
if (
|
|
370
|
-
options.unsafeMethodPolicy ===
|
|
371
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
|
|
372
|
-
) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const unsafeMethods = options.methods.filter((method) =>
|
|
376
|
-
UNSAFE_RETRY_METHODS.has(method.toUpperCase()),
|
|
377
|
-
);
|
|
378
|
-
if (unsafeMethods.length === 0) return;
|
|
379
|
-
|
|
380
|
-
throw new ProviderError(
|
|
381
|
-
`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
|
|
382
|
-
{ code: "retry_unsafe_method" },
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function isMethodRetryable(
|
|
387
|
-
method: HttpMethod,
|
|
388
|
-
options: NormalizedRetryOptions,
|
|
389
|
-
): boolean {
|
|
390
|
-
return options.methods
|
|
391
|
-
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
392
|
-
.includes(method.toUpperCase());
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function retryErrorCode(error: unknown): string | undefined {
|
|
396
|
-
if (error instanceof TransportError) {
|
|
397
|
-
return error.code;
|
|
398
|
-
}
|
|
399
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
400
|
-
const code = Reflect.get(error, "code");
|
|
401
|
-
return typeof code === "string" ? code : undefined;
|
|
402
|
-
}
|
|
403
|
-
return undefined;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function retryErrorStatus(error: unknown): number | undefined {
|
|
407
|
-
if (error instanceof TransportError) {
|
|
408
|
-
return error.status ?? error.upstreamStatus;
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function shouldRetryTransportError(
|
|
414
|
-
error: unknown,
|
|
415
|
-
options: NormalizedRetryOptions,
|
|
416
|
-
): boolean {
|
|
417
|
-
const code = retryErrorCode(error);
|
|
418
|
-
return Boolean(code && options.errorCodes.includes(code));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function retryAfterHeader(headers: Record<string, string>): string | undefined {
|
|
422
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
423
|
-
if (name.toLowerCase() === "retry-after") return value;
|
|
424
|
-
}
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function parseRetryAfterMs(
|
|
429
|
-
headers: Record<string, string>,
|
|
430
|
-
now: number = Date.now(),
|
|
431
|
-
): number | undefined {
|
|
432
|
-
const value = retryAfterHeader(headers);
|
|
433
|
-
if (!value) return undefined;
|
|
434
|
-
const seconds = Number(value);
|
|
435
|
-
if (Number.isFinite(seconds)) {
|
|
436
|
-
return Math.max(0, Math.floor(seconds * 1_000));
|
|
437
|
-
}
|
|
438
|
-
const dateMs = Date.parse(value);
|
|
439
|
-
if (!Number.isNaN(dateMs)) {
|
|
440
|
-
return Math.max(0, dateMs - now);
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function computeRetryDelayMs(
|
|
446
|
-
options: NormalizedRetryOptions,
|
|
447
|
-
attemptIndex: number,
|
|
448
|
-
headers?: Record<string, string>,
|
|
449
|
-
): number {
|
|
450
|
-
const multiplier =
|
|
451
|
-
options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
452
|
-
? 2 ** Math.max(0, attemptIndex - 1)
|
|
453
|
-
: 1;
|
|
454
|
-
const configuredDelay = Math.min(
|
|
455
|
-
options.baseDelayMs * multiplier,
|
|
456
|
-
options.maxDelayMs,
|
|
457
|
-
);
|
|
458
|
-
const retryAfterMs =
|
|
459
|
-
options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
460
|
-
? undefined
|
|
461
|
-
: headers
|
|
462
|
-
? parseRetryAfterMs(headers)
|
|
463
|
-
: undefined;
|
|
464
|
-
if (retryAfterMs !== undefined) {
|
|
465
|
-
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
466
|
-
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
467
|
-
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
468
|
-
}
|
|
469
|
-
return boundedRetryAfterMs;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
switch (options.jitter) {
|
|
473
|
-
case HttpRetryJitter.None:
|
|
474
|
-
return configuredDelay;
|
|
475
|
-
case HttpRetryJitter.Equal:
|
|
476
|
-
return Math.floor(
|
|
477
|
-
configuredDelay / 2 + Math.random() * (configuredDelay / 2),
|
|
478
|
-
);
|
|
479
|
-
case HttpRetryJitter.Full:
|
|
480
|
-
return Math.floor(Math.random() * configuredDelay);
|
|
481
|
-
}
|
|
67
|
+
function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is NativeHttpSkipOutcome {
|
|
68
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
482
69
|
}
|
|
483
70
|
|
|
484
71
|
async function sleep(ms: number): Promise<void> {
|
|
@@ -505,9 +92,7 @@ function withClientHeaders(
|
|
|
505
92
|
body: unknown,
|
|
506
93
|
): RequestOptions {
|
|
507
94
|
const headers: Record<string, string> = {
|
|
508
|
-
...(clientOptions.userAgent
|
|
509
|
-
? { "User-Agent": clientOptions.userAgent }
|
|
510
|
-
: {}),
|
|
95
|
+
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
511
96
|
...options?.headers,
|
|
512
97
|
};
|
|
513
98
|
|
|
@@ -522,10 +107,7 @@ function withClientHeaders(
|
|
|
522
107
|
}
|
|
523
108
|
|
|
524
109
|
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
525
|
-
const contentType =
|
|
526
|
-
headers["content-type"] ??
|
|
527
|
-
headers["Content-Type"] ??
|
|
528
|
-
headers["CONTENT-TYPE"];
|
|
110
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
529
111
|
|
|
530
112
|
if (contentType?.includes("application/json")) {
|
|
531
113
|
return body ? JSON.parse(body) : null;
|
|
@@ -596,23 +178,14 @@ async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
|
596
178
|
headers,
|
|
597
179
|
json: async <T = unknown>() => {
|
|
598
180
|
const contentType =
|
|
599
|
-
headers["content-type"] ??
|
|
600
|
-
|
|
601
|
-
headers["CONTENT-TYPE"];
|
|
602
|
-
return parseJson<T>(
|
|
603
|
-
contentType?.includes("application/json") && !rawText
|
|
604
|
-
? "null"
|
|
605
|
-
: rawText,
|
|
606
|
-
);
|
|
181
|
+
headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
182
|
+
return parseJson<T>(contentType?.includes("application/json") && !rawText ? "null" : rawText);
|
|
607
183
|
},
|
|
608
184
|
ok: response.status >= 200 && response.status < 300,
|
|
609
185
|
status: response.status,
|
|
610
186
|
text: async () => rawText,
|
|
611
187
|
arrayBuffer: async () =>
|
|
612
|
-
bodyBytes.buffer.slice(
|
|
613
|
-
bodyBytes.byteOffset,
|
|
614
|
-
bodyBytes.byteOffset + bodyBytes.byteLength,
|
|
615
|
-
),
|
|
188
|
+
bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
616
189
|
bytes: async () => bodyBytes.slice(0),
|
|
617
190
|
};
|
|
618
191
|
}
|
|
@@ -625,9 +198,7 @@ async function drainNativeResponseBody(response: Response): Promise<void> {
|
|
|
625
198
|
}
|
|
626
199
|
}
|
|
627
200
|
|
|
628
|
-
function requireNativeResponseBody(
|
|
629
|
-
response: Response,
|
|
630
|
-
): ReadableStream<Uint8Array> {
|
|
201
|
+
function requireNativeResponseBody(response: Response): ReadableStream<Uint8Array> {
|
|
631
202
|
if (!response.body) {
|
|
632
203
|
throw new TransportError("Response body stream is unavailable", {
|
|
633
204
|
code: "transport_stream_unavailable",
|
|
@@ -692,18 +263,19 @@ async function resolveNativeProxy(
|
|
|
692
263
|
warn: (message: string) => void,
|
|
693
264
|
proxyAttemptOffset = 0,
|
|
694
265
|
): Promise<string | undefined> {
|
|
695
|
-
const baseProxyAttempt =
|
|
696
|
-
clientOptions.proxyAttempt === undefined ||
|
|
697
|
-
!Number.isFinite(clientOptions.proxyAttempt)
|
|
698
|
-
? 0
|
|
699
|
-
: Math.max(0, Math.floor(clientOptions.proxyAttempt));
|
|
700
266
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
701
267
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
702
268
|
upstream: clientOptions.upstream,
|
|
703
269
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
704
270
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
705
271
|
affinityKey: clientOptions.affinityKey,
|
|
706
|
-
proxyAttempt:
|
|
272
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
273
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
274
|
+
retryAttemptOffset: proxyAttemptOffset,
|
|
275
|
+
}),
|
|
276
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
277
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
278
|
+
transportProtocols: ["http"],
|
|
707
279
|
telemetry: clientOptions.telemetry,
|
|
708
280
|
});
|
|
709
281
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -723,9 +295,7 @@ function assertNoHttpTransportOverrides(options: RequestOptions): void {
|
|
|
723
295
|
}
|
|
724
296
|
}
|
|
725
297
|
|
|
726
|
-
function normalizeNativeFetchBody(
|
|
727
|
-
body: unknown,
|
|
728
|
-
): string | ArrayBuffer | undefined {
|
|
298
|
+
function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefined {
|
|
729
299
|
const normalized = normalizeHttpRequestBody(body);
|
|
730
300
|
if (!Buffer.isBuffer(normalized)) {
|
|
731
301
|
return normalized;
|
|
@@ -744,11 +314,9 @@ async function fetchNativeHttp(
|
|
|
744
314
|
warn: (message: string) => void,
|
|
745
315
|
statusRetryCodes?: readonly number[],
|
|
746
316
|
proxyAttemptOffset = 0,
|
|
317
|
+
dedupe?: { attempted: Set<string> },
|
|
747
318
|
): Promise<NativeHttpAttemptOutcome> {
|
|
748
|
-
const requestUrl = appendQueryParams(
|
|
749
|
-
resolveHttpUrl(baseUrl, url),
|
|
750
|
-
options.params,
|
|
751
|
-
);
|
|
319
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
752
320
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
753
321
|
const timeoutHandle = options.timeout
|
|
754
322
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -756,12 +324,20 @@ async function fetchNativeHttp(
|
|
|
756
324
|
|
|
757
325
|
let proxy: string | undefined;
|
|
758
326
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
327
|
+
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
328
|
+
// failures are branded as TransportErrors and count against the request
|
|
329
|
+
// deadline, exactly as an inline resolve would.
|
|
330
|
+
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
331
|
+
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
332
|
+
// tried rather than re-issuing the same request. Returning the sentinel
|
|
333
|
+
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
334
|
+
// it crosses into the fallback vendor's pool span.
|
|
335
|
+
if (dedupe && proxy) {
|
|
336
|
+
if (dedupe.attempted.has(proxy)) {
|
|
337
|
+
return { kind: "dedupe-skip" };
|
|
338
|
+
}
|
|
339
|
+
dedupe.attempted.add(proxy);
|
|
340
|
+
}
|
|
765
341
|
const requestInit: NativeFetchInit = {
|
|
766
342
|
headers: options.headers,
|
|
767
343
|
method,
|
|
@@ -789,13 +365,10 @@ async function fetchNativeHttp(
|
|
|
789
365
|
|
|
790
366
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
791
367
|
await drainNativeResponseBody(response);
|
|
792
|
-
throw new TransportError(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
status: response.status,
|
|
797
|
-
},
|
|
798
|
-
);
|
|
368
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
369
|
+
code: "upstream_http_error",
|
|
370
|
+
status: response.status,
|
|
371
|
+
});
|
|
799
372
|
}
|
|
800
373
|
|
|
801
374
|
return toNativeHttpResponse(response);
|
|
@@ -819,10 +392,7 @@ async function fetchNativeHttpStream(
|
|
|
819
392
|
clientOptions: HttpClientOptions,
|
|
820
393
|
warn: (message: string) => void,
|
|
821
394
|
): Promise<HttpStreamResponse> {
|
|
822
|
-
const requestUrl = appendQueryParams(
|
|
823
|
-
resolveHttpUrl(baseUrl, url),
|
|
824
|
-
options.params,
|
|
825
|
-
);
|
|
395
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
826
396
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
827
397
|
const timeoutHandle = options.timeout
|
|
828
398
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -845,13 +415,10 @@ async function fetchNativeHttpStream(
|
|
|
845
415
|
|
|
846
416
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
847
417
|
await drainNativeResponseBody(response);
|
|
848
|
-
throw new TransportError(
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
status: response.status,
|
|
853
|
-
},
|
|
854
|
-
);
|
|
418
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
419
|
+
code: "upstream_http_error",
|
|
420
|
+
status: response.status,
|
|
421
|
+
});
|
|
855
422
|
}
|
|
856
423
|
|
|
857
424
|
return toNativeHttpStreamResponse(response);
|
|
@@ -891,23 +458,19 @@ export function createHttpClient(
|
|
|
891
458
|
);
|
|
892
459
|
}
|
|
893
460
|
assertNoHttpTransportOverrides(options);
|
|
894
|
-
const headersOptions = withClientHeaders(
|
|
895
|
-
options,
|
|
896
|
-
clientOptions,
|
|
897
|
-
options.body,
|
|
898
|
-
);
|
|
461
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
899
462
|
const methodName = normalizeHttpMethod(method);
|
|
900
463
|
const explicitRetry = headersOptions.retry !== undefined;
|
|
901
464
|
const retryOptions =
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (retryOptions)
|
|
465
|
+
normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
466
|
+
label: "HTTP",
|
|
467
|
+
}) ??
|
|
468
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
469
|
+
if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
907
470
|
const retryEnabled = Boolean(
|
|
908
471
|
retryOptions &&
|
|
909
472
|
retryOptions.attempts > 1 &&
|
|
910
|
-
|
|
473
|
+
isProxyTransportRetryMethod(methodName, retryOptions),
|
|
911
474
|
);
|
|
912
475
|
const statusRetryEnabled = Boolean(
|
|
913
476
|
retryEnabled &&
|
|
@@ -916,14 +479,63 @@ export function createHttpClient(
|
|
|
916
479
|
retryOptions.statusCodes.length > 0 &&
|
|
917
480
|
headersOptions.throwOnHttpError !== false,
|
|
918
481
|
);
|
|
919
|
-
const attemptOptions: RequestOptions & { body?: unknown } =
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
482
|
+
const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
|
|
483
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
484
|
+
: headersOptions;
|
|
485
|
+
|
|
486
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
487
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
488
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
489
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
490
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
491
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
492
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
493
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
494
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
495
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
496
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
497
|
+
// the full span.
|
|
498
|
+
const policyProxy: ProviderProxyPolicy | undefined = (() => {
|
|
499
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
500
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
501
|
+
})();
|
|
502
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
503
|
+
const transportAttemptCap = retryOptions
|
|
504
|
+
? resolvePolicyTransportAttemptCap({
|
|
505
|
+
policy: policyProxy,
|
|
506
|
+
usesPolicyAllocator,
|
|
507
|
+
retryAttempts: retryOptions.attempts,
|
|
508
|
+
explicitRetry,
|
|
509
|
+
method: methodName,
|
|
510
|
+
})
|
|
511
|
+
: 1;
|
|
512
|
+
|
|
513
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
514
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
515
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
516
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
517
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
518
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
519
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
520
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
521
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
522
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
523
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
524
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
525
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
526
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
527
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
528
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
529
|
+
policy: policyProxy,
|
|
530
|
+
usesPolicyAllocator,
|
|
531
|
+
explicitRetry,
|
|
532
|
+
method: methodName,
|
|
533
|
+
});
|
|
534
|
+
const dedupeContext = dedupeAllocatorEndpoints
|
|
535
|
+
? { attempted: new Set<string>() }
|
|
536
|
+
: undefined;
|
|
923
537
|
|
|
924
|
-
const executeOnce = (
|
|
925
|
-
proxyAttemptOffset = 0,
|
|
926
|
-
): Promise<NativeHttpAttemptOutcome> =>
|
|
538
|
+
const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
|
|
927
539
|
fetchNativeHttp(
|
|
928
540
|
baseUrl,
|
|
929
541
|
url,
|
|
@@ -933,44 +545,67 @@ export function createHttpClient(
|
|
|
933
545
|
warnOnce,
|
|
934
546
|
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
935
547
|
proxyAttemptOffset,
|
|
548
|
+
dedupeContext,
|
|
936
549
|
);
|
|
937
550
|
|
|
938
551
|
if (!retryEnabled || !retryOptions) {
|
|
939
552
|
const outcome = await executeOnce();
|
|
553
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
554
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
555
|
+
// but keep the union total.
|
|
556
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
557
|
+
code: "retry_exhausted",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
940
560
|
if (isHttpStatusOutcome(outcome)) {
|
|
941
561
|
throw toUpstreamHttpError(outcome.status);
|
|
942
562
|
}
|
|
943
563
|
return outcome;
|
|
944
564
|
}
|
|
945
565
|
|
|
566
|
+
let lastError: unknown;
|
|
946
567
|
let lastErrorCode: string | undefined;
|
|
947
568
|
let lastStatus: number | undefined;
|
|
948
|
-
|
|
569
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
570
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
571
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
572
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
573
|
+
// allocations skip offsets.
|
|
574
|
+
let issued = 0;
|
|
575
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
576
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
577
|
+
// so the catch counts a thrown *transport* failure once without
|
|
578
|
+
// double-counting a status outcome that already incremented before it
|
|
579
|
+
// re-threw as an upstream HTTP error.
|
|
580
|
+
let issuedThisAttempt = false;
|
|
949
581
|
try {
|
|
950
582
|
const outcome = await executeOnce(attempt - 1);
|
|
583
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
584
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
585
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
586
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
issued += 1;
|
|
590
|
+
issuedThisAttempt = true;
|
|
951
591
|
if (isHttpStatusOutcome(outcome)) {
|
|
952
592
|
lastStatus = outcome.status;
|
|
953
|
-
if (outcome.retryable &&
|
|
954
|
-
await sleep(
|
|
955
|
-
computeRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
956
|
-
);
|
|
593
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
594
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
957
595
|
continue;
|
|
958
596
|
}
|
|
959
597
|
throw toUpstreamHttpError(outcome.status);
|
|
960
598
|
}
|
|
961
599
|
|
|
962
600
|
const response = outcome;
|
|
963
|
-
if (
|
|
964
|
-
response.status >= 400 &&
|
|
965
|
-
headersOptions.throwOnHttpError !== false
|
|
966
|
-
) {
|
|
601
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
967
602
|
throw toUpstreamHttpError(response.status);
|
|
968
603
|
}
|
|
969
604
|
|
|
970
|
-
if (
|
|
605
|
+
if (issued > 1) {
|
|
971
606
|
const summary: HttpRetrySummary = {
|
|
972
|
-
attempts:
|
|
973
|
-
retries:
|
|
607
|
+
attempts: issued,
|
|
608
|
+
retries: issued - 1,
|
|
974
609
|
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
975
610
|
transport: "native",
|
|
976
611
|
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
@@ -980,21 +615,35 @@ export function createHttpClient(
|
|
|
980
615
|
}
|
|
981
616
|
return response;
|
|
982
617
|
} catch (error) {
|
|
983
|
-
|
|
984
|
-
|
|
618
|
+
if (!issuedThisAttempt) issued += 1;
|
|
619
|
+
lastError = error;
|
|
620
|
+
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
621
|
+
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
985
622
|
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
986
623
|
if (
|
|
987
|
-
attempt <
|
|
988
|
-
(
|
|
989
|
-
|
|
624
|
+
attempt < transportAttemptCap &&
|
|
625
|
+
shouldRetryProxyTransportAttempt({
|
|
626
|
+
error,
|
|
627
|
+
explicitRetry,
|
|
628
|
+
method: methodName,
|
|
629
|
+
options: retryOptions,
|
|
630
|
+
proxyUsed,
|
|
631
|
+
})
|
|
990
632
|
) {
|
|
991
|
-
await sleep(
|
|
633
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
992
634
|
continue;
|
|
993
635
|
}
|
|
994
636
|
throw error;
|
|
995
637
|
}
|
|
996
638
|
}
|
|
997
639
|
|
|
640
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
641
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
642
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
643
|
+
// failure rather than a synthetic exhaustion error.
|
|
644
|
+
if (lastError !== undefined) {
|
|
645
|
+
throw lastError;
|
|
646
|
+
}
|
|
998
647
|
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
999
648
|
code: "retry_exhausted",
|
|
1000
649
|
});
|
|
@@ -1012,30 +661,17 @@ export function createHttpClient(
|
|
|
1012
661
|
);
|
|
1013
662
|
}
|
|
1014
663
|
assertNoHttpTransportOverrides(options);
|
|
1015
|
-
const headersOptions = withClientHeaders(
|
|
1016
|
-
options,
|
|
1017
|
-
clientOptions,
|
|
1018
|
-
options.body,
|
|
1019
|
-
);
|
|
664
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
1020
665
|
const methodName = normalizeHttpMethod(method);
|
|
1021
|
-
return fetchNativeHttpStream(
|
|
1022
|
-
baseUrl,
|
|
1023
|
-
url,
|
|
1024
|
-
methodName,
|
|
1025
|
-
headersOptions,
|
|
1026
|
-
clientOptions,
|
|
1027
|
-
warnOnce,
|
|
1028
|
-
);
|
|
666
|
+
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
1029
667
|
}
|
|
1030
668
|
|
|
1031
669
|
return {
|
|
1032
670
|
request: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1033
671
|
request(url, options.method ?? "GET", options),
|
|
1034
672
|
get: async (url, options) => request(url, "GET", options),
|
|
1035
|
-
post: async (url, body, options) =>
|
|
1036
|
-
|
|
1037
|
-
put: async (url, body, options) =>
|
|
1038
|
-
request(url, "PUT", { ...options, body }),
|
|
673
|
+
post: async (url, body, options) => request(url, "POST", { ...options, body }),
|
|
674
|
+
put: async (url, body, options) => request(url, "PUT", { ...options, body }),
|
|
1039
675
|
delete: async (url, options) => request(url, "DELETE", options),
|
|
1040
676
|
stream: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1041
677
|
streamRequest(url, options.method ?? "GET", options),
|