@apifuse/provider-sdk 2.1.0-beta.9 → 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 +240 -0
- package/CHANGELOG.md +93 -0
- package/README.md +26 -10
- package/SUBMISSION.md +11 -12
- 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 +36 -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 +2177 -353
- 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/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +427 -0
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +14 -48
- 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 +47 -33
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- 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 +5 -6
- package/dist/define.d.ts +13 -1
- package/dist/define.js +245 -178
- 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 -41
- package/dist/index.js +39 -36
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +35 -15
- package/dist/provider.d.ts +11 -9
- package/dist/provider.js +9 -8
- 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 +4 -2
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +214 -25
- 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 +160 -344
- 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 +7 -4
- package/dist/runtime/stealth.js +257 -215
- 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 -3
- package/dist/server/index.js +6 -2
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +199 -0
- package/dist/server/self-test.js +1113 -0
- package/dist/server/serve.d.ts +14 -3
- package/dist/server/serve.js +135 -64
- 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 +12 -15
- package/dist/types.d.ts +237 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +728 -0
- package/src/ceremonies/index.ts +33 -121
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +69 -99
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- 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 +13 -28
- package/src/define.ts +397 -528
- 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 +64 -41
- package/src/lint.ts +122 -159
- package/src/provider.ts +40 -9
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +6 -6
- package/src/runtime/browser.ts +320 -151
- 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 +216 -539
- 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 +331 -369
- 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 +44 -3
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +1450 -0
- package/src/server/serve.ts +206 -216
- 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 +24 -64
- package/src/types.ts +274 -1
- package/src/user-input.ts +118 -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,445 +38,34 @@ 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;
|
|
59
44
|
headers: Record<string, string>;
|
|
60
45
|
retryable: boolean;
|
|
46
|
+
proxyUsed: boolean;
|
|
61
47
|
};
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"transport_timeout",
|
|
73
|
-
] as const;
|
|
74
|
-
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
|
|
75
|
-
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503] as const;
|
|
76
|
-
const KNOWN_RETRY_METHODS = new Set([
|
|
77
|
-
"GET",
|
|
78
|
-
"HEAD",
|
|
79
|
-
"POST",
|
|
80
|
-
"PUT",
|
|
81
|
-
"DELETE",
|
|
82
|
-
"OPTIONS",
|
|
83
|
-
"TRACE",
|
|
84
|
-
"PATCH",
|
|
85
|
-
]);
|
|
86
|
-
const UNSAFE_RETRY_METHODS = new Set([
|
|
87
|
-
"POST",
|
|
88
|
-
"PUT",
|
|
89
|
-
"PATCH",
|
|
90
|
-
"DELETE",
|
|
91
|
-
"TRACE",
|
|
92
|
-
]);
|
|
93
|
-
const MAX_RETRY_ATTEMPTS = 8;
|
|
94
|
-
const MAX_RETRY_DELAY_MS = 30_000;
|
|
95
|
-
|
|
96
|
-
function hasOwnValue<T extends string>(
|
|
97
|
-
values: Record<string, T>,
|
|
98
|
-
value: unknown,
|
|
99
|
-
): value is T {
|
|
100
|
-
if (typeof value !== "string") return false;
|
|
101
|
-
return Object.values(values).some((candidate) => candidate === value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function createInvalidRetryPolicyError(message: string): ProviderError {
|
|
105
|
-
return new ProviderError(message, { code: "retry_invalid_policy" });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function createRetryOptions(preset: HttpRetryPreset): NormalizedRetryOptions {
|
|
109
|
-
switch (preset) {
|
|
110
|
-
case HttpRetryPreset.Off:
|
|
111
|
-
return {
|
|
112
|
-
preset,
|
|
113
|
-
attempts: 1,
|
|
114
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
115
|
-
statusCodes: [],
|
|
116
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
117
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
118
|
-
baseDelayMs: 100,
|
|
119
|
-
maxDelayMs: 1_000,
|
|
120
|
-
jitter: HttpRetryJitter.Full,
|
|
121
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
122
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
123
|
-
};
|
|
124
|
-
case HttpRetryPreset.SafeRead:
|
|
125
|
-
return {
|
|
126
|
-
preset,
|
|
127
|
-
attempts: 3,
|
|
128
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
129
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
130
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
131
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
132
|
-
baseDelayMs: 100,
|
|
133
|
-
maxDelayMs: 2_000,
|
|
134
|
-
jitter: HttpRetryJitter.Full,
|
|
135
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
136
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
137
|
-
};
|
|
138
|
-
case HttpRetryPreset.AggressiveRead:
|
|
139
|
-
return {
|
|
140
|
-
preset,
|
|
141
|
-
attempts: 4,
|
|
142
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
143
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
144
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
145
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
146
|
-
baseDelayMs: 150,
|
|
147
|
-
maxDelayMs: 5_000,
|
|
148
|
-
jitter: HttpRetryJitter.Full,
|
|
149
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
150
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
151
|
-
};
|
|
152
|
-
case HttpRetryPreset.RateLimitAware:
|
|
153
|
-
return {
|
|
154
|
-
preset,
|
|
155
|
-
attempts: 3,
|
|
156
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
157
|
-
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
158
|
-
errorCodes: ["transport_timeout"],
|
|
159
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
160
|
-
baseDelayMs: 250,
|
|
161
|
-
maxDelayMs: 5_000,
|
|
162
|
-
jitter: HttpRetryJitter.Equal,
|
|
163
|
-
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
164
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
165
|
-
};
|
|
166
|
-
case HttpRetryPreset.TransportTransient:
|
|
167
|
-
return {
|
|
168
|
-
preset,
|
|
169
|
-
attempts: 3,
|
|
170
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
171
|
-
statusCodes: [],
|
|
172
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
173
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
174
|
-
baseDelayMs: 100,
|
|
175
|
-
maxDelayMs: 1_000,
|
|
176
|
-
jitter: HttpRetryJitter.Full,
|
|
177
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
178
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function clampPositiveInteger(
|
|
185
|
-
value: number | undefined,
|
|
186
|
-
fallback: number,
|
|
187
|
-
max: number,
|
|
188
|
-
): number {
|
|
189
|
-
if (value === undefined) return fallback;
|
|
190
|
-
if (!Number.isFinite(value) || value < 1) return fallback;
|
|
191
|
-
return Math.min(Math.floor(value), max);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function clampDelay(value: number | undefined, fallback: number): number {
|
|
195
|
-
if (value === undefined) return fallback;
|
|
196
|
-
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
197
|
-
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function normalizeRetryOptions(
|
|
201
|
-
retry: RequestOptions["retry"],
|
|
202
|
-
): NormalizedRetryOptions | undefined {
|
|
203
|
-
if (retry === undefined || retry === false) {
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
if (retry === true) {
|
|
207
|
-
return createRetryOptions(HttpRetryPreset.TransportTransient);
|
|
208
|
-
}
|
|
209
|
-
if (typeof retry === "string") {
|
|
210
|
-
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
211
|
-
throw createInvalidRetryPolicyError(
|
|
212
|
-
`Unknown HTTP retry preset: ${retry}`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return createRetryOptions(retry);
|
|
216
|
-
}
|
|
217
|
-
if (typeof retry !== "object" || retry === null) {
|
|
218
|
-
throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
|
|
219
|
-
}
|
|
220
|
-
if (Array.isArray(retry)) {
|
|
221
|
-
throw createInvalidRetryPolicyError(
|
|
222
|
-
"HTTP retry policy must be a plain object",
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
validateRetryOptionsShape(retry);
|
|
227
|
-
const base = createRetryOptions(
|
|
228
|
-
retry.preset ?? HttpRetryPreset.TransportTransient,
|
|
229
|
-
);
|
|
230
|
-
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
231
|
-
const normalized: NormalizedRetryOptions = {
|
|
232
|
-
preset: retry.preset ?? base.preset,
|
|
233
|
-
attempts: clampPositiveInteger(
|
|
234
|
-
retry.attempts,
|
|
235
|
-
base.attempts,
|
|
236
|
-
MAX_RETRY_ATTEMPTS,
|
|
237
|
-
),
|
|
238
|
-
methods:
|
|
239
|
-
retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
240
|
-
statusCodes:
|
|
241
|
-
retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
|
|
242
|
-
base.statusCodes,
|
|
243
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
244
|
-
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
245
|
-
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
246
|
-
maxDelayMs,
|
|
247
|
-
jitter: retry.jitter ?? base.jitter,
|
|
248
|
-
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
249
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
return normalized;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function validateRetryOptionsShape(retry: HttpRetryOptions): void {
|
|
256
|
-
if (
|
|
257
|
-
retry.preset !== undefined &&
|
|
258
|
-
!hasOwnValue(HttpRetryPreset, retry.preset)
|
|
259
|
-
) {
|
|
260
|
-
throw createInvalidRetryPolicyError(
|
|
261
|
-
`Unknown HTTP retry preset: ${String(retry.preset)}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (
|
|
265
|
-
retry.delayStrategy !== undefined &&
|
|
266
|
-
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)
|
|
267
|
-
) {
|
|
268
|
-
throw createInvalidRetryPolicyError(
|
|
269
|
-
`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`,
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
if (
|
|
273
|
-
retry.jitter !== undefined &&
|
|
274
|
-
!hasOwnValue(HttpRetryJitter, retry.jitter)
|
|
275
|
-
) {
|
|
276
|
-
throw createInvalidRetryPolicyError(
|
|
277
|
-
`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
if (
|
|
281
|
-
retry.retryAfter !== undefined &&
|
|
282
|
-
!hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)
|
|
283
|
-
) {
|
|
284
|
-
throw createInvalidRetryPolicyError(
|
|
285
|
-
`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
if (
|
|
289
|
-
retry.unsafeMethodPolicy !== undefined &&
|
|
290
|
-
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)
|
|
291
|
-
) {
|
|
292
|
-
throw createInvalidRetryPolicyError(
|
|
293
|
-
`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
if (retry.methods !== undefined) {
|
|
297
|
-
if (!Array.isArray(retry.methods)) {
|
|
298
|
-
throw createInvalidRetryPolicyError(
|
|
299
|
-
"HTTP retry methods must be an array",
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
const nonStringMethods = retry.methods.filter(
|
|
303
|
-
(method) => typeof method !== "string",
|
|
304
|
-
);
|
|
305
|
-
if (nonStringMethods.length > 0) {
|
|
306
|
-
throw createInvalidRetryPolicyError(
|
|
307
|
-
"HTTP retry methods must contain only strings",
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
const unknownMethods = retry.methods
|
|
311
|
-
.map((method) => method.toUpperCase())
|
|
312
|
-
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
313
|
-
if (unknownMethods.length > 0) {
|
|
314
|
-
throw createInvalidRetryPolicyError(
|
|
315
|
-
`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (retry.statusCodes !== undefined) {
|
|
320
|
-
if (!Array.isArray(retry.statusCodes)) {
|
|
321
|
-
throw createInvalidRetryPolicyError(
|
|
322
|
-
"HTTP retry statusCodes must be an array",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const invalidStatusCodes = retry.statusCodes.filter(
|
|
326
|
-
(status) =>
|
|
327
|
-
!Number.isInteger(status) ||
|
|
328
|
-
Number(status) < 100 ||
|
|
329
|
-
Number(status) > 599,
|
|
330
|
-
);
|
|
331
|
-
if (invalidStatusCodes.length > 0) {
|
|
332
|
-
throw createInvalidRetryPolicyError(
|
|
333
|
-
"HTTP retry statusCodes must contain HTTP status integers in [100, 599]",
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (retry.errorCodes !== undefined) {
|
|
338
|
-
if (!Array.isArray(retry.errorCodes)) {
|
|
339
|
-
throw createInvalidRetryPolicyError(
|
|
340
|
-
"HTTP retry errorCodes must be an array",
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
const nonStringErrorCodes = retry.errorCodes.filter(
|
|
344
|
-
(errorCode) => typeof errorCode !== "string",
|
|
345
|
-
);
|
|
346
|
-
if (nonStringErrorCodes.length > 0) {
|
|
347
|
-
throw createInvalidRetryPolicyError(
|
|
348
|
-
"HTTP retry errorCodes must contain only strings",
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (
|
|
353
|
-
retry.preset === HttpRetryPreset.Off &&
|
|
354
|
-
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
355
|
-
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))
|
|
356
|
-
) {
|
|
357
|
-
throw createInvalidRetryPolicyError(
|
|
358
|
-
"HTTP retry preset off cannot be combined with retry-enabling overrides",
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function validateUnsafeRetryMethods(options: NormalizedRetryOptions): void {
|
|
364
|
-
if (
|
|
365
|
-
options.unsafeMethodPolicy ===
|
|
366
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
|
|
367
|
-
) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
const unsafeMethods = options.methods.filter((method) =>
|
|
371
|
-
UNSAFE_RETRY_METHODS.has(method.toUpperCase()),
|
|
372
|
-
);
|
|
373
|
-
if (unsafeMethods.length === 0) return;
|
|
374
|
-
|
|
375
|
-
throw new ProviderError(
|
|
376
|
-
`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
|
|
377
|
-
{ code: "retry_unsafe_method" },
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function isMethodRetryable(
|
|
382
|
-
method: HttpMethod,
|
|
383
|
-
options: NormalizedRetryOptions,
|
|
384
|
-
): boolean {
|
|
385
|
-
return options.methods
|
|
386
|
-
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
387
|
-
.includes(method.toUpperCase());
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function retryErrorCode(error: unknown): string | undefined {
|
|
391
|
-
if (error instanceof TransportError) {
|
|
392
|
-
return error.code;
|
|
393
|
-
}
|
|
394
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
395
|
-
const code = Reflect.get(error, "code");
|
|
396
|
-
return typeof code === "string" ? code : undefined;
|
|
397
|
-
}
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
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" };
|
|
400
58
|
|
|
401
|
-
|
|
402
|
-
if (error instanceof TransportError) {
|
|
403
|
-
return error.status ?? error.upstreamStatus;
|
|
404
|
-
}
|
|
405
|
-
return undefined;
|
|
406
|
-
}
|
|
59
|
+
type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome | NativeHttpSkipOutcome;
|
|
407
60
|
|
|
408
|
-
|
|
409
|
-
error: unknown,
|
|
410
|
-
options: NormalizedRetryOptions,
|
|
411
|
-
): boolean {
|
|
412
|
-
const code = retryErrorCode(error);
|
|
413
|
-
return Boolean(code && options.errorCodes.includes(code));
|
|
414
|
-
}
|
|
61
|
+
type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
|
|
415
62
|
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
if (name.toLowerCase() === "retry-after") return value;
|
|
419
|
-
}
|
|
420
|
-
return undefined;
|
|
63
|
+
function isHttpStatusOutcome(outcome: NativeHttpAttemptOutcome): outcome is HttpStatusOutcome {
|
|
64
|
+
return "kind" in outcome && outcome.kind === "http-status";
|
|
421
65
|
}
|
|
422
66
|
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
now: number = Date.now(),
|
|
426
|
-
): number | undefined {
|
|
427
|
-
const value = retryAfterHeader(headers);
|
|
428
|
-
if (!value) return undefined;
|
|
429
|
-
const seconds = Number(value);
|
|
430
|
-
if (Number.isFinite(seconds)) {
|
|
431
|
-
return Math.max(0, Math.floor(seconds * 1_000));
|
|
432
|
-
}
|
|
433
|
-
const dateMs = Date.parse(value);
|
|
434
|
-
if (!Number.isNaN(dateMs)) {
|
|
435
|
-
return Math.max(0, dateMs - now);
|
|
436
|
-
}
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function computeRetryDelayMs(
|
|
441
|
-
options: NormalizedRetryOptions,
|
|
442
|
-
attemptIndex: number,
|
|
443
|
-
headers?: Record<string, string>,
|
|
444
|
-
): number {
|
|
445
|
-
const multiplier =
|
|
446
|
-
options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
447
|
-
? 2 ** Math.max(0, attemptIndex - 1)
|
|
448
|
-
: 1;
|
|
449
|
-
const configuredDelay = Math.min(
|
|
450
|
-
options.baseDelayMs * multiplier,
|
|
451
|
-
options.maxDelayMs,
|
|
452
|
-
);
|
|
453
|
-
const retryAfterMs =
|
|
454
|
-
options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
455
|
-
? undefined
|
|
456
|
-
: headers
|
|
457
|
-
? parseRetryAfterMs(headers)
|
|
458
|
-
: undefined;
|
|
459
|
-
if (retryAfterMs !== undefined) {
|
|
460
|
-
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
461
|
-
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
462
|
-
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
463
|
-
}
|
|
464
|
-
return boundedRetryAfterMs;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
switch (options.jitter) {
|
|
468
|
-
case HttpRetryJitter.None:
|
|
469
|
-
return configuredDelay;
|
|
470
|
-
case HttpRetryJitter.Equal:
|
|
471
|
-
return Math.floor(
|
|
472
|
-
configuredDelay / 2 + Math.random() * (configuredDelay / 2),
|
|
473
|
-
);
|
|
474
|
-
case HttpRetryJitter.Full:
|
|
475
|
-
return Math.floor(Math.random() * configuredDelay);
|
|
476
|
-
}
|
|
67
|
+
function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is NativeHttpSkipOutcome {
|
|
68
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
477
69
|
}
|
|
478
70
|
|
|
479
71
|
async function sleep(ms: number): Promise<void> {
|
|
@@ -500,9 +92,7 @@ function withClientHeaders(
|
|
|
500
92
|
body: unknown,
|
|
501
93
|
): RequestOptions {
|
|
502
94
|
const headers: Record<string, string> = {
|
|
503
|
-
...(clientOptions.userAgent
|
|
504
|
-
? { "User-Agent": clientOptions.userAgent }
|
|
505
|
-
: {}),
|
|
95
|
+
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
506
96
|
...options?.headers,
|
|
507
97
|
};
|
|
508
98
|
|
|
@@ -517,10 +107,7 @@ function withClientHeaders(
|
|
|
517
107
|
}
|
|
518
108
|
|
|
519
109
|
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
520
|
-
const contentType =
|
|
521
|
-
headers["content-type"] ??
|
|
522
|
-
headers["Content-Type"] ??
|
|
523
|
-
headers["CONTENT-TYPE"];
|
|
110
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
524
111
|
|
|
525
112
|
if (contentType?.includes("application/json")) {
|
|
526
113
|
return body ? JSON.parse(body) : null;
|
|
@@ -582,7 +169,8 @@ function toHttpTransportError(error: unknown): TransportError {
|
|
|
582
169
|
|
|
583
170
|
async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
584
171
|
const headers = Object.fromEntries(response.headers.entries());
|
|
585
|
-
const
|
|
172
|
+
const bodyBytes = new Uint8Array(await response.arrayBuffer());
|
|
173
|
+
const rawText = new TextDecoder().decode(bodyBytes);
|
|
586
174
|
const data = parseHttpData(rawText, headers);
|
|
587
175
|
|
|
588
176
|
return {
|
|
@@ -590,18 +178,15 @@ async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
|
590
178
|
headers,
|
|
591
179
|
json: async <T = unknown>() => {
|
|
592
180
|
const contentType =
|
|
593
|
-
headers["content-type"] ??
|
|
594
|
-
|
|
595
|
-
headers["CONTENT-TYPE"];
|
|
596
|
-
return parseJson<T>(
|
|
597
|
-
contentType?.includes("application/json") && !rawText
|
|
598
|
-
? "null"
|
|
599
|
-
: rawText,
|
|
600
|
-
);
|
|
181
|
+
headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
182
|
+
return parseJson<T>(contentType?.includes("application/json") && !rawText ? "null" : rawText);
|
|
601
183
|
},
|
|
602
184
|
ok: response.status >= 200 && response.status < 300,
|
|
603
185
|
status: response.status,
|
|
604
186
|
text: async () => rawText,
|
|
187
|
+
arrayBuffer: async () =>
|
|
188
|
+
bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
189
|
+
bytes: async () => bodyBytes.slice(0),
|
|
605
190
|
};
|
|
606
191
|
}
|
|
607
192
|
|
|
@@ -613,9 +198,7 @@ async function drainNativeResponseBody(response: Response): Promise<void> {
|
|
|
613
198
|
}
|
|
614
199
|
}
|
|
615
200
|
|
|
616
|
-
function requireNativeResponseBody(
|
|
617
|
-
response: Response,
|
|
618
|
-
): ReadableStream<Uint8Array> {
|
|
201
|
+
function requireNativeResponseBody(response: Response): ReadableStream<Uint8Array> {
|
|
619
202
|
if (!response.body) {
|
|
620
203
|
throw new TransportError("Response body stream is unavailable", {
|
|
621
204
|
code: "transport_stream_unavailable",
|
|
@@ -678,12 +261,21 @@ async function resolveNativeProxy(
|
|
|
678
261
|
options: RequestOptions,
|
|
679
262
|
clientOptions: HttpClientOptions,
|
|
680
263
|
warn: (message: string) => void,
|
|
264
|
+
proxyAttemptOffset = 0,
|
|
681
265
|
): Promise<string | undefined> {
|
|
682
266
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
683
267
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
684
268
|
upstream: clientOptions.upstream,
|
|
685
269
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
270
|
+
proxyPolicy: clientOptions.proxyPolicy,
|
|
686
271
|
affinityKey: clientOptions.affinityKey,
|
|
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"],
|
|
687
279
|
telemetry: clientOptions.telemetry,
|
|
688
280
|
});
|
|
689
281
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -703,9 +295,7 @@ function assertNoHttpTransportOverrides(options: RequestOptions): void {
|
|
|
703
295
|
}
|
|
704
296
|
}
|
|
705
297
|
|
|
706
|
-
function normalizeNativeFetchBody(
|
|
707
|
-
body: unknown,
|
|
708
|
-
): string | ArrayBuffer | undefined {
|
|
298
|
+
function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefined {
|
|
709
299
|
const normalized = normalizeHttpRequestBody(body);
|
|
710
300
|
if (!Buffer.isBuffer(normalized)) {
|
|
711
301
|
return normalized;
|
|
@@ -723,18 +313,31 @@ async function fetchNativeHttp(
|
|
|
723
313
|
clientOptions: HttpClientOptions,
|
|
724
314
|
warn: (message: string) => void,
|
|
725
315
|
statusRetryCodes?: readonly number[],
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
);
|
|
316
|
+
proxyAttemptOffset = 0,
|
|
317
|
+
dedupe?: { attempted: Set<string> },
|
|
318
|
+
): Promise<NativeHttpAttemptOutcome> {
|
|
319
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
731
320
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
732
321
|
const timeoutHandle = options.timeout
|
|
733
322
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
734
323
|
: undefined;
|
|
735
324
|
|
|
325
|
+
let proxy: string | undefined;
|
|
736
326
|
try {
|
|
737
|
-
|
|
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
|
+
}
|
|
738
341
|
const requestInit: NativeFetchInit = {
|
|
739
342
|
headers: options.headers,
|
|
740
343
|
method,
|
|
@@ -756,18 +359,16 @@ async function fetchNativeHttp(
|
|
|
756
359
|
status: response.status,
|
|
757
360
|
headers,
|
|
758
361
|
retryable: statusRetryCodes.includes(response.status),
|
|
362
|
+
proxyUsed: Boolean(proxy),
|
|
759
363
|
};
|
|
760
364
|
}
|
|
761
365
|
|
|
762
366
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
763
367
|
await drainNativeResponseBody(response);
|
|
764
|
-
throw new TransportError(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
status: response.status,
|
|
769
|
-
},
|
|
770
|
-
);
|
|
368
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
369
|
+
code: "upstream_http_error",
|
|
370
|
+
status: response.status,
|
|
371
|
+
});
|
|
771
372
|
}
|
|
772
373
|
|
|
773
374
|
return toNativeHttpResponse(response);
|
|
@@ -775,7 +376,9 @@ async function fetchNativeHttp(
|
|
|
775
376
|
if (error instanceof SyntaxError) {
|
|
776
377
|
throw error;
|
|
777
378
|
}
|
|
778
|
-
|
|
379
|
+
const transportError = toHttpTransportError(error) as NativeHttpAttemptError;
|
|
380
|
+
transportError.proxyUsed = Boolean(proxy);
|
|
381
|
+
throw transportError;
|
|
779
382
|
} finally {
|
|
780
383
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
781
384
|
}
|
|
@@ -789,10 +392,7 @@ async function fetchNativeHttpStream(
|
|
|
789
392
|
clientOptions: HttpClientOptions,
|
|
790
393
|
warn: (message: string) => void,
|
|
791
394
|
): Promise<HttpStreamResponse> {
|
|
792
|
-
const requestUrl = appendQueryParams(
|
|
793
|
-
resolveHttpUrl(baseUrl, url),
|
|
794
|
-
options.params,
|
|
795
|
-
);
|
|
395
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
796
396
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
797
397
|
const timeoutHandle = options.timeout
|
|
798
398
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -815,13 +415,10 @@ async function fetchNativeHttpStream(
|
|
|
815
415
|
|
|
816
416
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
817
417
|
await drainNativeResponseBody(response);
|
|
818
|
-
throw new TransportError(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
status: response.status,
|
|
823
|
-
},
|
|
824
|
-
);
|
|
418
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
419
|
+
code: "upstream_http_error",
|
|
420
|
+
status: response.status,
|
|
421
|
+
});
|
|
825
422
|
}
|
|
826
423
|
|
|
827
424
|
return toNativeHttpStreamResponse(response);
|
|
@@ -861,31 +458,84 @@ export function createHttpClient(
|
|
|
861
458
|
);
|
|
862
459
|
}
|
|
863
460
|
assertNoHttpTransportOverrides(options);
|
|
864
|
-
const headersOptions = withClientHeaders(
|
|
865
|
-
options,
|
|
866
|
-
clientOptions,
|
|
867
|
-
options.body,
|
|
868
|
-
);
|
|
461
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
869
462
|
const methodName = normalizeHttpMethod(method);
|
|
870
|
-
const
|
|
871
|
-
|
|
463
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
464
|
+
const retryOptions =
|
|
465
|
+
normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
466
|
+
label: "HTTP",
|
|
467
|
+
}) ??
|
|
468
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
469
|
+
if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
872
470
|
const retryEnabled = Boolean(
|
|
873
471
|
retryOptions &&
|
|
874
472
|
retryOptions.attempts > 1 &&
|
|
875
|
-
|
|
473
|
+
isProxyTransportRetryMethod(methodName, retryOptions),
|
|
876
474
|
);
|
|
877
475
|
const statusRetryEnabled = Boolean(
|
|
878
476
|
retryEnabled &&
|
|
477
|
+
explicitRetry &&
|
|
879
478
|
retryOptions &&
|
|
880
479
|
retryOptions.statusCodes.length > 0 &&
|
|
881
480
|
headersOptions.throwOnHttpError !== false,
|
|
882
481
|
);
|
|
883
|
-
const attemptOptions: RequestOptions & { body?: unknown } =
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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;
|
|
887
512
|
|
|
888
|
-
|
|
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;
|
|
537
|
+
|
|
538
|
+
const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
|
|
889
539
|
fetchNativeHttp(
|
|
890
540
|
baseUrl,
|
|
891
541
|
url,
|
|
@@ -894,44 +544,68 @@ export function createHttpClient(
|
|
|
894
544
|
clientOptions,
|
|
895
545
|
warnOnce,
|
|
896
546
|
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
547
|
+
proxyAttemptOffset,
|
|
548
|
+
dedupeContext,
|
|
897
549
|
);
|
|
898
550
|
|
|
899
551
|
if (!retryEnabled || !retryOptions) {
|
|
900
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
|
+
}
|
|
901
560
|
if (isHttpStatusOutcome(outcome)) {
|
|
902
561
|
throw toUpstreamHttpError(outcome.status);
|
|
903
562
|
}
|
|
904
563
|
return outcome;
|
|
905
564
|
}
|
|
906
565
|
|
|
566
|
+
let lastError: unknown;
|
|
907
567
|
let lastErrorCode: string | undefined;
|
|
908
568
|
let lastStatus: number | undefined;
|
|
909
|
-
|
|
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;
|
|
910
581
|
try {
|
|
911
|
-
const outcome = await executeOnce();
|
|
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;
|
|
912
591
|
if (isHttpStatusOutcome(outcome)) {
|
|
913
592
|
lastStatus = outcome.status;
|
|
914
|
-
if (outcome.retryable &&
|
|
915
|
-
await sleep(
|
|
916
|
-
computeRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
917
|
-
);
|
|
593
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
594
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
918
595
|
continue;
|
|
919
596
|
}
|
|
920
597
|
throw toUpstreamHttpError(outcome.status);
|
|
921
598
|
}
|
|
922
599
|
|
|
923
600
|
const response = outcome;
|
|
924
|
-
if (
|
|
925
|
-
response.status >= 400 &&
|
|
926
|
-
headersOptions.throwOnHttpError !== false
|
|
927
|
-
) {
|
|
601
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
928
602
|
throw toUpstreamHttpError(response.status);
|
|
929
603
|
}
|
|
930
604
|
|
|
931
|
-
if (
|
|
605
|
+
if (issued > 1) {
|
|
932
606
|
const summary: HttpRetrySummary = {
|
|
933
|
-
attempts:
|
|
934
|
-
retries:
|
|
607
|
+
attempts: issued,
|
|
608
|
+
retries: issued - 1,
|
|
935
609
|
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
936
610
|
transport: "native",
|
|
937
611
|
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
@@ -941,19 +615,35 @@ export function createHttpClient(
|
|
|
941
615
|
}
|
|
942
616
|
return response;
|
|
943
617
|
} catch (error) {
|
|
944
|
-
|
|
945
|
-
|
|
618
|
+
if (!issuedThisAttempt) issued += 1;
|
|
619
|
+
lastError = error;
|
|
620
|
+
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
621
|
+
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
622
|
+
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
946
623
|
if (
|
|
947
|
-
attempt <
|
|
948
|
-
|
|
624
|
+
attempt < transportAttemptCap &&
|
|
625
|
+
shouldRetryProxyTransportAttempt({
|
|
626
|
+
error,
|
|
627
|
+
explicitRetry,
|
|
628
|
+
method: methodName,
|
|
629
|
+
options: retryOptions,
|
|
630
|
+
proxyUsed,
|
|
631
|
+
})
|
|
949
632
|
) {
|
|
950
|
-
await sleep(
|
|
633
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
951
634
|
continue;
|
|
952
635
|
}
|
|
953
636
|
throw error;
|
|
954
637
|
}
|
|
955
638
|
}
|
|
956
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
|
+
}
|
|
957
647
|
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
958
648
|
code: "retry_exhausted",
|
|
959
649
|
});
|
|
@@ -971,30 +661,17 @@ export function createHttpClient(
|
|
|
971
661
|
);
|
|
972
662
|
}
|
|
973
663
|
assertNoHttpTransportOverrides(options);
|
|
974
|
-
const headersOptions = withClientHeaders(
|
|
975
|
-
options,
|
|
976
|
-
clientOptions,
|
|
977
|
-
options.body,
|
|
978
|
-
);
|
|
664
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
979
665
|
const methodName = normalizeHttpMethod(method);
|
|
980
|
-
return fetchNativeHttpStream(
|
|
981
|
-
baseUrl,
|
|
982
|
-
url,
|
|
983
|
-
methodName,
|
|
984
|
-
headersOptions,
|
|
985
|
-
clientOptions,
|
|
986
|
-
warnOnce,
|
|
987
|
-
);
|
|
666
|
+
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
988
667
|
}
|
|
989
668
|
|
|
990
669
|
return {
|
|
991
670
|
request: async (url, options: RequestWithMethodOptions = {}) =>
|
|
992
671
|
request(url, options.method ?? "GET", options),
|
|
993
672
|
get: async (url, options) => request(url, "GET", options),
|
|
994
|
-
post: async (url, body, options) =>
|
|
995
|
-
|
|
996
|
-
put: async (url, body, options) =>
|
|
997
|
-
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 }),
|
|
998
675
|
delete: async (url, options) => request(url, "DELETE", options),
|
|
999
676
|
stream: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1000
677
|
streamRequest(url, options.method ?? "GET", options),
|