@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/dist/runtime/http.js
CHANGED
|
@@ -1,316 +1,14 @@
|
|
|
1
|
-
import { resolveProxyConfigAsync } from "../config/loader";
|
|
2
|
-
import { ProviderError, TransportError } from "../errors";
|
|
3
|
-
import { parseSseStream, readableBytes, readableLines, readableTextChunks
|
|
4
|
-
import {
|
|
5
|
-
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
|
|
1
|
+
import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
|
|
2
|
+
import { ProviderError, TransportError } from "../errors.js";
|
|
3
|
+
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
4
|
+
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
5
|
+
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
|
|
6
6
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
7
7
|
function isHttpStatusOutcome(outcome) {
|
|
8
8
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"transport_network_error",
|
|
13
|
-
"transport_timeout",
|
|
14
|
-
];
|
|
15
|
-
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
16
|
-
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503];
|
|
17
|
-
const KNOWN_RETRY_METHODS = new Set([
|
|
18
|
-
"GET",
|
|
19
|
-
"HEAD",
|
|
20
|
-
"POST",
|
|
21
|
-
"PUT",
|
|
22
|
-
"DELETE",
|
|
23
|
-
"OPTIONS",
|
|
24
|
-
"TRACE",
|
|
25
|
-
"PATCH",
|
|
26
|
-
]);
|
|
27
|
-
const UNSAFE_RETRY_METHODS = new Set([
|
|
28
|
-
"POST",
|
|
29
|
-
"PUT",
|
|
30
|
-
"PATCH",
|
|
31
|
-
"DELETE",
|
|
32
|
-
"TRACE",
|
|
33
|
-
]);
|
|
34
|
-
const MAX_RETRY_ATTEMPTS = 8;
|
|
35
|
-
const MAX_RETRY_DELAY_MS = 30_000;
|
|
36
|
-
function hasOwnValue(values, value) {
|
|
37
|
-
if (typeof value !== "string")
|
|
38
|
-
return false;
|
|
39
|
-
return Object.values(values).some((candidate) => candidate === value);
|
|
40
|
-
}
|
|
41
|
-
function createInvalidRetryPolicyError(message) {
|
|
42
|
-
return new ProviderError(message, { code: "retry_invalid_policy" });
|
|
43
|
-
}
|
|
44
|
-
function createRetryOptions(preset) {
|
|
45
|
-
switch (preset) {
|
|
46
|
-
case HttpRetryPreset.Off:
|
|
47
|
-
return {
|
|
48
|
-
preset,
|
|
49
|
-
attempts: 1,
|
|
50
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
51
|
-
statusCodes: [],
|
|
52
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
53
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
54
|
-
baseDelayMs: 100,
|
|
55
|
-
maxDelayMs: 1_000,
|
|
56
|
-
jitter: HttpRetryJitter.Full,
|
|
57
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
58
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
59
|
-
};
|
|
60
|
-
case HttpRetryPreset.SafeRead:
|
|
61
|
-
return {
|
|
62
|
-
preset,
|
|
63
|
-
attempts: 3,
|
|
64
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
65
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
66
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
67
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
68
|
-
baseDelayMs: 100,
|
|
69
|
-
maxDelayMs: 2_000,
|
|
70
|
-
jitter: HttpRetryJitter.Full,
|
|
71
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
72
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
73
|
-
};
|
|
74
|
-
case HttpRetryPreset.AggressiveRead:
|
|
75
|
-
return {
|
|
76
|
-
preset,
|
|
77
|
-
attempts: 4,
|
|
78
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
79
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
80
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
81
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
82
|
-
baseDelayMs: 150,
|
|
83
|
-
maxDelayMs: 5_000,
|
|
84
|
-
jitter: HttpRetryJitter.Full,
|
|
85
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
86
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
87
|
-
};
|
|
88
|
-
case HttpRetryPreset.RateLimitAware:
|
|
89
|
-
return {
|
|
90
|
-
preset,
|
|
91
|
-
attempts: 3,
|
|
92
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
93
|
-
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
94
|
-
errorCodes: ["transport_timeout"],
|
|
95
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
96
|
-
baseDelayMs: 250,
|
|
97
|
-
maxDelayMs: 5_000,
|
|
98
|
-
jitter: HttpRetryJitter.Equal,
|
|
99
|
-
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
100
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
101
|
-
};
|
|
102
|
-
case HttpRetryPreset.TransportTransient:
|
|
103
|
-
return {
|
|
104
|
-
preset,
|
|
105
|
-
attempts: 3,
|
|
106
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
107
|
-
statusCodes: [],
|
|
108
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
109
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
110
|
-
baseDelayMs: 100,
|
|
111
|
-
maxDelayMs: 1_000,
|
|
112
|
-
jitter: HttpRetryJitter.Full,
|
|
113
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
114
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
|
|
118
|
-
}
|
|
119
|
-
function clampPositiveInteger(value, fallback, max) {
|
|
120
|
-
if (value === undefined)
|
|
121
|
-
return fallback;
|
|
122
|
-
if (!Number.isFinite(value) || value < 1)
|
|
123
|
-
return fallback;
|
|
124
|
-
return Math.min(Math.floor(value), max);
|
|
125
|
-
}
|
|
126
|
-
function clampDelay(value, fallback) {
|
|
127
|
-
if (value === undefined)
|
|
128
|
-
return fallback;
|
|
129
|
-
if (!Number.isFinite(value) || value < 0)
|
|
130
|
-
return fallback;
|
|
131
|
-
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
132
|
-
}
|
|
133
|
-
function normalizeRetryOptions(retry) {
|
|
134
|
-
if (retry === undefined || retry === false) {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
if (retry === true) {
|
|
138
|
-
return createRetryOptions(HttpRetryPreset.TransportTransient);
|
|
139
|
-
}
|
|
140
|
-
if (typeof retry === "string") {
|
|
141
|
-
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
142
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${retry}`);
|
|
143
|
-
}
|
|
144
|
-
return createRetryOptions(retry);
|
|
145
|
-
}
|
|
146
|
-
if (typeof retry !== "object" || retry === null) {
|
|
147
|
-
throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
|
|
148
|
-
}
|
|
149
|
-
if (Array.isArray(retry)) {
|
|
150
|
-
throw createInvalidRetryPolicyError("HTTP retry policy must be a plain object");
|
|
151
|
-
}
|
|
152
|
-
validateRetryOptionsShape(retry);
|
|
153
|
-
const base = createRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient);
|
|
154
|
-
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
155
|
-
const normalized = {
|
|
156
|
-
preset: retry.preset ?? base.preset,
|
|
157
|
-
attempts: clampPositiveInteger(retry.attempts, base.attempts, MAX_RETRY_ATTEMPTS),
|
|
158
|
-
methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
159
|
-
statusCodes: retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
|
|
160
|
-
base.statusCodes,
|
|
161
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
162
|
-
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
163
|
-
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
164
|
-
maxDelayMs,
|
|
165
|
-
jitter: retry.jitter ?? base.jitter,
|
|
166
|
-
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
167
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
168
|
-
};
|
|
169
|
-
return normalized;
|
|
170
|
-
}
|
|
171
|
-
function validateRetryOptionsShape(retry) {
|
|
172
|
-
if (retry.preset !== undefined &&
|
|
173
|
-
!hasOwnValue(HttpRetryPreset, retry.preset)) {
|
|
174
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${String(retry.preset)}`);
|
|
175
|
-
}
|
|
176
|
-
if (retry.delayStrategy !== undefined &&
|
|
177
|
-
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)) {
|
|
178
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`);
|
|
179
|
-
}
|
|
180
|
-
if (retry.jitter !== undefined &&
|
|
181
|
-
!hasOwnValue(HttpRetryJitter, retry.jitter)) {
|
|
182
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`);
|
|
183
|
-
}
|
|
184
|
-
if (retry.retryAfter !== undefined &&
|
|
185
|
-
!hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
|
|
186
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`);
|
|
187
|
-
}
|
|
188
|
-
if (retry.unsafeMethodPolicy !== undefined &&
|
|
189
|
-
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)) {
|
|
190
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`);
|
|
191
|
-
}
|
|
192
|
-
if (retry.methods !== undefined) {
|
|
193
|
-
if (!Array.isArray(retry.methods)) {
|
|
194
|
-
throw createInvalidRetryPolicyError("HTTP retry methods must be an array");
|
|
195
|
-
}
|
|
196
|
-
const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
|
|
197
|
-
if (nonStringMethods.length > 0) {
|
|
198
|
-
throw createInvalidRetryPolicyError("HTTP retry methods must contain only strings");
|
|
199
|
-
}
|
|
200
|
-
const unknownMethods = retry.methods
|
|
201
|
-
.map((method) => method.toUpperCase())
|
|
202
|
-
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
203
|
-
if (unknownMethods.length > 0) {
|
|
204
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (retry.statusCodes !== undefined) {
|
|
208
|
-
if (!Array.isArray(retry.statusCodes)) {
|
|
209
|
-
throw createInvalidRetryPolicyError("HTTP retry statusCodes must be an array");
|
|
210
|
-
}
|
|
211
|
-
const invalidStatusCodes = retry.statusCodes.filter((status) => !Number.isInteger(status) ||
|
|
212
|
-
Number(status) < 100 ||
|
|
213
|
-
Number(status) > 599);
|
|
214
|
-
if (invalidStatusCodes.length > 0) {
|
|
215
|
-
throw createInvalidRetryPolicyError("HTTP retry statusCodes must contain HTTP status integers in [100, 599]");
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (retry.errorCodes !== undefined) {
|
|
219
|
-
if (!Array.isArray(retry.errorCodes)) {
|
|
220
|
-
throw createInvalidRetryPolicyError("HTTP retry errorCodes must be an array");
|
|
221
|
-
}
|
|
222
|
-
const nonStringErrorCodes = retry.errorCodes.filter((errorCode) => typeof errorCode !== "string");
|
|
223
|
-
if (nonStringErrorCodes.length > 0) {
|
|
224
|
-
throw createInvalidRetryPolicyError("HTTP retry errorCodes must contain only strings");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (retry.preset === HttpRetryPreset.Off &&
|
|
228
|
-
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
229
|
-
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))) {
|
|
230
|
-
throw createInvalidRetryPolicyError("HTTP retry preset off cannot be combined with retry-enabling overrides");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function validateUnsafeRetryMethods(options) {
|
|
234
|
-
if (options.unsafeMethodPolicy ===
|
|
235
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const unsafeMethods = options.methods.filter((method) => UNSAFE_RETRY_METHODS.has(method.toUpperCase()));
|
|
239
|
-
if (unsafeMethods.length === 0)
|
|
240
|
-
return;
|
|
241
|
-
throw new ProviderError(`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
|
|
242
|
-
}
|
|
243
|
-
function isMethodRetryable(method, options) {
|
|
244
|
-
return options.methods
|
|
245
|
-
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
246
|
-
.includes(method.toUpperCase());
|
|
247
|
-
}
|
|
248
|
-
function retryErrorCode(error) {
|
|
249
|
-
if (error instanceof TransportError) {
|
|
250
|
-
return error.code;
|
|
251
|
-
}
|
|
252
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
253
|
-
const code = Reflect.get(error, "code");
|
|
254
|
-
return typeof code === "string" ? code : undefined;
|
|
255
|
-
}
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
function retryErrorStatus(error) {
|
|
259
|
-
if (error instanceof TransportError) {
|
|
260
|
-
return error.status ?? error.upstreamStatus;
|
|
261
|
-
}
|
|
262
|
-
return undefined;
|
|
263
|
-
}
|
|
264
|
-
function shouldRetryTransportError(error, options) {
|
|
265
|
-
const code = retryErrorCode(error);
|
|
266
|
-
return Boolean(code && options.errorCodes.includes(code));
|
|
267
|
-
}
|
|
268
|
-
function retryAfterHeader(headers) {
|
|
269
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
270
|
-
if (name.toLowerCase() === "retry-after")
|
|
271
|
-
return value;
|
|
272
|
-
}
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
function parseRetryAfterMs(headers, now = Date.now()) {
|
|
276
|
-
const value = retryAfterHeader(headers);
|
|
277
|
-
if (!value)
|
|
278
|
-
return undefined;
|
|
279
|
-
const seconds = Number(value);
|
|
280
|
-
if (Number.isFinite(seconds)) {
|
|
281
|
-
return Math.max(0, Math.floor(seconds * 1_000));
|
|
282
|
-
}
|
|
283
|
-
const dateMs = Date.parse(value);
|
|
284
|
-
if (!Number.isNaN(dateMs)) {
|
|
285
|
-
return Math.max(0, dateMs - now);
|
|
286
|
-
}
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
function computeRetryDelayMs(options, attemptIndex, headers) {
|
|
290
|
-
const multiplier = options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
291
|
-
? 2 ** Math.max(0, attemptIndex - 1)
|
|
292
|
-
: 1;
|
|
293
|
-
const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
|
|
294
|
-
const retryAfterMs = options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
295
|
-
? undefined
|
|
296
|
-
: headers
|
|
297
|
-
? parseRetryAfterMs(headers)
|
|
298
|
-
: undefined;
|
|
299
|
-
if (retryAfterMs !== undefined) {
|
|
300
|
-
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
301
|
-
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
302
|
-
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
303
|
-
}
|
|
304
|
-
return boundedRetryAfterMs;
|
|
305
|
-
}
|
|
306
|
-
switch (options.jitter) {
|
|
307
|
-
case HttpRetryJitter.None:
|
|
308
|
-
return configuredDelay;
|
|
309
|
-
case HttpRetryJitter.Equal:
|
|
310
|
-
return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
|
|
311
|
-
case HttpRetryJitter.Full:
|
|
312
|
-
return Math.floor(Math.random() * configuredDelay);
|
|
313
|
-
}
|
|
10
|
+
function isDedupeSkipOutcome(outcome) {
|
|
11
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
314
12
|
}
|
|
315
13
|
async function sleep(ms) {
|
|
316
14
|
if (ms <= 0)
|
|
@@ -330,9 +28,7 @@ function hasHeader(headers, name) {
|
|
|
330
28
|
}
|
|
331
29
|
function withClientHeaders(options, clientOptions, body) {
|
|
332
30
|
const headers = {
|
|
333
|
-
...(clientOptions.userAgent
|
|
334
|
-
? { "User-Agent": clientOptions.userAgent }
|
|
335
|
-
: {}),
|
|
31
|
+
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
336
32
|
...options?.headers,
|
|
337
33
|
};
|
|
338
34
|
if (body !== undefined && !hasHeader(headers, "Content-Type")) {
|
|
@@ -344,9 +40,7 @@ function withClientHeaders(options, clientOptions, body) {
|
|
|
344
40
|
};
|
|
345
41
|
}
|
|
346
42
|
function parseHttpData(body, headers) {
|
|
347
|
-
const contentType = headers["content-type"] ??
|
|
348
|
-
headers["Content-Type"] ??
|
|
349
|
-
headers["CONTENT-TYPE"];
|
|
43
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
350
44
|
if (contentType?.includes("application/json")) {
|
|
351
45
|
return body ? JSON.parse(body) : null;
|
|
352
46
|
}
|
|
@@ -396,22 +90,21 @@ function toHttpTransportError(error) {
|
|
|
396
90
|
}
|
|
397
91
|
async function toNativeHttpResponse(response) {
|
|
398
92
|
const headers = Object.fromEntries(response.headers.entries());
|
|
399
|
-
const
|
|
93
|
+
const bodyBytes = new Uint8Array(await response.arrayBuffer());
|
|
94
|
+
const rawText = new TextDecoder().decode(bodyBytes);
|
|
400
95
|
const data = parseHttpData(rawText, headers);
|
|
401
96
|
return {
|
|
402
97
|
data,
|
|
403
98
|
headers,
|
|
404
99
|
json: async () => {
|
|
405
|
-
const contentType = headers["content-type"] ??
|
|
406
|
-
|
|
407
|
-
headers["CONTENT-TYPE"];
|
|
408
|
-
return parseJson(contentType?.includes("application/json") && !rawText
|
|
409
|
-
? "null"
|
|
410
|
-
: rawText);
|
|
100
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
101
|
+
return parseJson(contentType?.includes("application/json") && !rawText ? "null" : rawText);
|
|
411
102
|
},
|
|
412
103
|
ok: response.status >= 200 && response.status < 300,
|
|
413
104
|
status: response.status,
|
|
414
105
|
text: async () => rawText,
|
|
106
|
+
arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
107
|
+
bytes: async () => bodyBytes.slice(0),
|
|
415
108
|
};
|
|
416
109
|
}
|
|
417
110
|
async function drainNativeResponseBody(response) {
|
|
@@ -474,12 +167,20 @@ function isAbsoluteUrl(url) {
|
|
|
474
167
|
function resolveHttpUrl(baseUrl, url) {
|
|
475
168
|
return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
|
|
476
169
|
}
|
|
477
|
-
async function resolveNativeProxy(options, clientOptions, warn) {
|
|
170
|
+
async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
|
|
478
171
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
479
172
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
480
173
|
upstream: clientOptions.upstream,
|
|
481
174
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
175
|
+
proxyPolicy: clientOptions.proxyPolicy,
|
|
482
176
|
affinityKey: clientOptions.affinityKey,
|
|
177
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
178
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
179
|
+
retryAttemptOffset: proxyAttemptOffset,
|
|
180
|
+
}),
|
|
181
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
182
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
183
|
+
transportProtocols: ["http"],
|
|
483
184
|
telemetry: clientOptions.telemetry,
|
|
484
185
|
});
|
|
485
186
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -501,14 +202,28 @@ function normalizeNativeFetchBody(body) {
|
|
|
501
202
|
copied.set(normalized);
|
|
502
203
|
return copied.buffer;
|
|
503
204
|
}
|
|
504
|
-
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes) {
|
|
205
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
|
|
505
206
|
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
506
207
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
507
208
|
const timeoutHandle = options.timeout
|
|
508
209
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
509
210
|
: undefined;
|
|
211
|
+
let proxy;
|
|
510
212
|
try {
|
|
511
|
-
|
|
213
|
+
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
214
|
+
// failures are branded as TransportErrors and count against the request
|
|
215
|
+
// deadline, exactly as an inline resolve would.
|
|
216
|
+
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
217
|
+
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
218
|
+
// tried rather than re-issuing the same request. Returning the sentinel
|
|
219
|
+
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
220
|
+
// it crosses into the fallback vendor's pool span.
|
|
221
|
+
if (dedupe && proxy) {
|
|
222
|
+
if (dedupe.attempted.has(proxy)) {
|
|
223
|
+
return { kind: "dedupe-skip" };
|
|
224
|
+
}
|
|
225
|
+
dedupe.attempted.add(proxy);
|
|
226
|
+
}
|
|
512
227
|
const requestInit = {
|
|
513
228
|
headers: options.headers,
|
|
514
229
|
method,
|
|
@@ -529,6 +244,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
529
244
|
status: response.status,
|
|
530
245
|
headers,
|
|
531
246
|
retryable: statusRetryCodes.includes(response.status),
|
|
247
|
+
proxyUsed: Boolean(proxy),
|
|
532
248
|
};
|
|
533
249
|
}
|
|
534
250
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
@@ -544,7 +260,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
544
260
|
if (error instanceof SyntaxError) {
|
|
545
261
|
throw error;
|
|
546
262
|
}
|
|
547
|
-
|
|
263
|
+
const transportError = toHttpTransportError(error);
|
|
264
|
+
transportError.proxyUsed = Boolean(proxy);
|
|
265
|
+
throw transportError;
|
|
548
266
|
}
|
|
549
267
|
finally {
|
|
550
268
|
if (timeoutHandle)
|
|
@@ -608,49 +326,130 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
608
326
|
assertNoHttpTransportOverrides(options);
|
|
609
327
|
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
610
328
|
const methodName = normalizeHttpMethod(method);
|
|
611
|
-
const
|
|
329
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
330
|
+
const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
331
|
+
label: "HTTP",
|
|
332
|
+
}) ??
|
|
333
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
612
334
|
if (retryOptions)
|
|
613
|
-
|
|
335
|
+
validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
614
336
|
const retryEnabled = Boolean(retryOptions &&
|
|
615
337
|
retryOptions.attempts > 1 &&
|
|
616
|
-
|
|
338
|
+
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
617
339
|
const statusRetryEnabled = Boolean(retryEnabled &&
|
|
340
|
+
explicitRetry &&
|
|
618
341
|
retryOptions &&
|
|
619
342
|
retryOptions.statusCodes.length > 0 &&
|
|
620
343
|
headersOptions.throwOnHttpError !== false);
|
|
621
344
|
const attemptOptions = statusRetryEnabled
|
|
622
345
|
? { ...headersOptions, throwOnHttpError: false }
|
|
623
346
|
: headersOptions;
|
|
624
|
-
|
|
347
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
348
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
349
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
350
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
351
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
352
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
353
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
354
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
355
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
356
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
357
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
358
|
+
// the full span.
|
|
359
|
+
const policyProxy = (() => {
|
|
360
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
361
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
362
|
+
})();
|
|
363
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
364
|
+
const transportAttemptCap = retryOptions
|
|
365
|
+
? resolvePolicyTransportAttemptCap({
|
|
366
|
+
policy: policyProxy,
|
|
367
|
+
usesPolicyAllocator,
|
|
368
|
+
retryAttempts: retryOptions.attempts,
|
|
369
|
+
explicitRetry,
|
|
370
|
+
method: methodName,
|
|
371
|
+
})
|
|
372
|
+
: 1;
|
|
373
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
374
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
375
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
376
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
377
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
378
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
379
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
380
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
381
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
382
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
383
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
384
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
385
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
386
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
387
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
388
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
389
|
+
policy: policyProxy,
|
|
390
|
+
usesPolicyAllocator,
|
|
391
|
+
explicitRetry,
|
|
392
|
+
method: methodName,
|
|
393
|
+
});
|
|
394
|
+
const dedupeContext = dedupeAllocatorEndpoints
|
|
395
|
+
? { attempted: new Set() }
|
|
396
|
+
: undefined;
|
|
397
|
+
const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
|
|
625
398
|
if (!retryEnabled || !retryOptions) {
|
|
626
399
|
const outcome = await executeOnce();
|
|
400
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
401
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
402
|
+
// but keep the union total.
|
|
403
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
404
|
+
code: "retry_exhausted",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
627
407
|
if (isHttpStatusOutcome(outcome)) {
|
|
628
408
|
throw toUpstreamHttpError(outcome.status);
|
|
629
409
|
}
|
|
630
410
|
return outcome;
|
|
631
411
|
}
|
|
412
|
+
let lastError;
|
|
632
413
|
let lastErrorCode;
|
|
633
414
|
let lastStatus;
|
|
634
|
-
|
|
415
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
416
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
417
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
418
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
419
|
+
// allocations skip offsets.
|
|
420
|
+
let issued = 0;
|
|
421
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
422
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
423
|
+
// so the catch counts a thrown *transport* failure once without
|
|
424
|
+
// double-counting a status outcome that already incremented before it
|
|
425
|
+
// re-threw as an upstream HTTP error.
|
|
426
|
+
let issuedThisAttempt = false;
|
|
635
427
|
try {
|
|
636
|
-
const outcome = await executeOnce();
|
|
428
|
+
const outcome = await executeOnce(attempt - 1);
|
|
429
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
430
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
431
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
432
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
issued += 1;
|
|
436
|
+
issuedThisAttempt = true;
|
|
637
437
|
if (isHttpStatusOutcome(outcome)) {
|
|
638
438
|
lastStatus = outcome.status;
|
|
639
|
-
if (outcome.retryable &&
|
|
640
|
-
await sleep(
|
|
439
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
440
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
641
441
|
continue;
|
|
642
442
|
}
|
|
643
443
|
throw toUpstreamHttpError(outcome.status);
|
|
644
444
|
}
|
|
645
445
|
const response = outcome;
|
|
646
|
-
if (response.status >= 400 &&
|
|
647
|
-
headersOptions.throwOnHttpError !== false) {
|
|
446
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
648
447
|
throw toUpstreamHttpError(response.status);
|
|
649
448
|
}
|
|
650
|
-
if (
|
|
449
|
+
if (issued > 1) {
|
|
651
450
|
const summary = {
|
|
652
|
-
attempts:
|
|
653
|
-
retries:
|
|
451
|
+
attempts: issued,
|
|
452
|
+
retries: issued - 1,
|
|
654
453
|
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
655
454
|
transport: "native",
|
|
656
455
|
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
@@ -661,16 +460,33 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
661
460
|
return response;
|
|
662
461
|
}
|
|
663
462
|
catch (error) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
463
|
+
if (!issuedThisAttempt)
|
|
464
|
+
issued += 1;
|
|
465
|
+
lastError = error;
|
|
466
|
+
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
467
|
+
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
468
|
+
const proxyUsed = Boolean(error.proxyUsed);
|
|
469
|
+
if (attempt < transportAttemptCap &&
|
|
470
|
+
shouldRetryProxyTransportAttempt({
|
|
471
|
+
error,
|
|
472
|
+
explicitRetry,
|
|
473
|
+
method: methodName,
|
|
474
|
+
options: retryOptions,
|
|
475
|
+
proxyUsed,
|
|
476
|
+
})) {
|
|
477
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
669
478
|
continue;
|
|
670
479
|
}
|
|
671
480
|
throw error;
|
|
672
481
|
}
|
|
673
482
|
}
|
|
483
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
484
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
485
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
486
|
+
// failure rather than a synthetic exhaustion error.
|
|
487
|
+
if (lastError !== undefined) {
|
|
488
|
+
throw lastError;
|
|
489
|
+
}
|
|
674
490
|
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
675
491
|
code: "retry_exhausted",
|
|
676
492
|
});
|