@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2
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 +134 -0
- package/CHANGELOG.md +61 -0
- package/README.md +21 -9
- package/SUBMISSION.md +10 -11
- package/bin/apifuse-pack-check.ts +22 -0
- package/bin/apifuse-submit-check.ts +1747 -388
- 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 +436 -0
- package/dist/ceremonies/index.js +7 -31
- package/dist/cli/create.js +45 -30
- 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 +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -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/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/contract.js +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.js +140 -70
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +27 -0
- package/dist/provider.d.ts +4 -2
- package/dist/provider.js +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +203 -0
- package/dist/runtime/http.js +46 -336
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +206 -206
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- 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 +98 -0
- package/dist/server/self-test.js +555 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.js +38 -10
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/dist/testing/run.js +5 -1
- package/dist/types.d.ts +150 -0
- package/package.json +9 -2
- 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 +786 -0
- package/src/ceremonies/index.ts +9 -43
- package/src/cli/create.ts +60 -97
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +198 -71
- package/src/index.ts +16 -0
- package/src/lint.ts +33 -0
- package/src/provider.ts +27 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +293 -1
- package/src/runtime/http.ts +84 -530
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +269 -353
- package/src/server/index.ts +36 -0
- 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 +725 -0
- package/src/server/serve.ts +75 -6
- package/src/server/types.ts +1 -0
- package/src/testing/run.ts +9 -1
- package/src/types.ts +186 -0
package/dist/runtime/http.js
CHANGED
|
@@ -1,317 +1,12 @@
|
|
|
1
1
|
import { resolveProxyConfigAsync } from "../config/loader";
|
|
2
2
|
import { ProviderError, TransportError } from "../errors";
|
|
3
|
-
import { parseSseStream, readableBytes, readableLines, readableTextChunks
|
|
4
|
-
import {
|
|
3
|
+
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream";
|
|
4
|
+
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy";
|
|
5
5
|
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
|
|
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
|
-
const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
11
|
-
const DEFAULT_RETRY_ERROR_CODES = [
|
|
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
|
-
}
|
|
314
|
-
}
|
|
315
10
|
async function sleep(ms) {
|
|
316
11
|
if (ms <= 0)
|
|
317
12
|
return;
|
|
@@ -330,9 +25,7 @@ function hasHeader(headers, name) {
|
|
|
330
25
|
}
|
|
331
26
|
function withClientHeaders(options, clientOptions, body) {
|
|
332
27
|
const headers = {
|
|
333
|
-
...(clientOptions.userAgent
|
|
334
|
-
? { "User-Agent": clientOptions.userAgent }
|
|
335
|
-
: {}),
|
|
28
|
+
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
336
29
|
...options?.headers,
|
|
337
30
|
};
|
|
338
31
|
if (body !== undefined && !hasHeader(headers, "Content-Type")) {
|
|
@@ -344,9 +37,7 @@ function withClientHeaders(options, clientOptions, body) {
|
|
|
344
37
|
};
|
|
345
38
|
}
|
|
346
39
|
function parseHttpData(body, headers) {
|
|
347
|
-
const contentType = headers["content-type"] ??
|
|
348
|
-
headers["Content-Type"] ??
|
|
349
|
-
headers["CONTENT-TYPE"];
|
|
40
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
350
41
|
if (contentType?.includes("application/json")) {
|
|
351
42
|
return body ? JSON.parse(body) : null;
|
|
352
43
|
}
|
|
@@ -396,22 +87,21 @@ function toHttpTransportError(error) {
|
|
|
396
87
|
}
|
|
397
88
|
async function toNativeHttpResponse(response) {
|
|
398
89
|
const headers = Object.fromEntries(response.headers.entries());
|
|
399
|
-
const
|
|
90
|
+
const bodyBytes = new Uint8Array(await response.arrayBuffer());
|
|
91
|
+
const rawText = new TextDecoder().decode(bodyBytes);
|
|
400
92
|
const data = parseHttpData(rawText, headers);
|
|
401
93
|
return {
|
|
402
94
|
data,
|
|
403
95
|
headers,
|
|
404
96
|
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);
|
|
97
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
98
|
+
return parseJson(contentType?.includes("application/json") && !rawText ? "null" : rawText);
|
|
411
99
|
},
|
|
412
100
|
ok: response.status >= 200 && response.status < 300,
|
|
413
101
|
status: response.status,
|
|
414
102
|
text: async () => rawText,
|
|
103
|
+
arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
104
|
+
bytes: async () => bodyBytes.slice(0),
|
|
415
105
|
};
|
|
416
106
|
}
|
|
417
107
|
async function drainNativeResponseBody(response) {
|
|
@@ -474,12 +164,17 @@ function isAbsoluteUrl(url) {
|
|
|
474
164
|
function resolveHttpUrl(baseUrl, url) {
|
|
475
165
|
return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
|
|
476
166
|
}
|
|
477
|
-
async function resolveNativeProxy(options, clientOptions, warn) {
|
|
167
|
+
async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
|
|
478
168
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
479
169
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
480
170
|
upstream: clientOptions.upstream,
|
|
481
171
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
172
|
+
proxyPolicy: clientOptions.proxyPolicy,
|
|
482
173
|
affinityKey: clientOptions.affinityKey,
|
|
174
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
175
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
176
|
+
retryAttemptOffset: proxyAttemptOffset,
|
|
177
|
+
}),
|
|
483
178
|
telemetry: clientOptions.telemetry,
|
|
484
179
|
});
|
|
485
180
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -501,14 +196,15 @@ function normalizeNativeFetchBody(body) {
|
|
|
501
196
|
copied.set(normalized);
|
|
502
197
|
return copied.buffer;
|
|
503
198
|
}
|
|
504
|
-
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes) {
|
|
199
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
|
|
505
200
|
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
506
201
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
507
202
|
const timeoutHandle = options.timeout
|
|
508
203
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
509
204
|
: undefined;
|
|
205
|
+
let proxy;
|
|
510
206
|
try {
|
|
511
|
-
|
|
207
|
+
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
512
208
|
const requestInit = {
|
|
513
209
|
headers: options.headers,
|
|
514
210
|
method,
|
|
@@ -529,6 +225,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
529
225
|
status: response.status,
|
|
530
226
|
headers,
|
|
531
227
|
retryable: statusRetryCodes.includes(response.status),
|
|
228
|
+
proxyUsed: Boolean(proxy),
|
|
532
229
|
};
|
|
533
230
|
}
|
|
534
231
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
@@ -544,7 +241,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
544
241
|
if (error instanceof SyntaxError) {
|
|
545
242
|
throw error;
|
|
546
243
|
}
|
|
547
|
-
|
|
244
|
+
const transportError = toHttpTransportError(error);
|
|
245
|
+
transportError.proxyUsed = Boolean(proxy);
|
|
246
|
+
throw transportError;
|
|
548
247
|
}
|
|
549
248
|
finally {
|
|
550
249
|
if (timeoutHandle)
|
|
@@ -608,20 +307,25 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
608
307
|
assertNoHttpTransportOverrides(options);
|
|
609
308
|
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
610
309
|
const methodName = normalizeHttpMethod(method);
|
|
611
|
-
const
|
|
310
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
311
|
+
const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
312
|
+
label: "HTTP",
|
|
313
|
+
}) ??
|
|
314
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
612
315
|
if (retryOptions)
|
|
613
|
-
|
|
316
|
+
validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
614
317
|
const retryEnabled = Boolean(retryOptions &&
|
|
615
318
|
retryOptions.attempts > 1 &&
|
|
616
|
-
|
|
319
|
+
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
617
320
|
const statusRetryEnabled = Boolean(retryEnabled &&
|
|
321
|
+
explicitRetry &&
|
|
618
322
|
retryOptions &&
|
|
619
323
|
retryOptions.statusCodes.length > 0 &&
|
|
620
324
|
headersOptions.throwOnHttpError !== false);
|
|
621
325
|
const attemptOptions = statusRetryEnabled
|
|
622
326
|
? { ...headersOptions, throwOnHttpError: false }
|
|
623
327
|
: headersOptions;
|
|
624
|
-
const executeOnce = () => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined);
|
|
328
|
+
const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
|
|
625
329
|
if (!retryEnabled || !retryOptions) {
|
|
626
330
|
const outcome = await executeOnce();
|
|
627
331
|
if (isHttpStatusOutcome(outcome)) {
|
|
@@ -633,18 +337,17 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
633
337
|
let lastStatus;
|
|
634
338
|
for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
|
|
635
339
|
try {
|
|
636
|
-
const outcome = await executeOnce();
|
|
340
|
+
const outcome = await executeOnce(attempt - 1);
|
|
637
341
|
if (isHttpStatusOutcome(outcome)) {
|
|
638
342
|
lastStatus = outcome.status;
|
|
639
343
|
if (outcome.retryable && attempt < retryOptions.attempts) {
|
|
640
|
-
await sleep(
|
|
344
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
641
345
|
continue;
|
|
642
346
|
}
|
|
643
347
|
throw toUpstreamHttpError(outcome.status);
|
|
644
348
|
}
|
|
645
349
|
const response = outcome;
|
|
646
|
-
if (response.status >= 400 &&
|
|
647
|
-
headersOptions.throwOnHttpError !== false) {
|
|
350
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
648
351
|
throw toUpstreamHttpError(response.status);
|
|
649
352
|
}
|
|
650
353
|
if (attempt > 1) {
|
|
@@ -661,11 +364,18 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
661
364
|
return response;
|
|
662
365
|
}
|
|
663
366
|
catch (error) {
|
|
664
|
-
lastErrorCode =
|
|
665
|
-
lastStatus =
|
|
367
|
+
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
368
|
+
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
369
|
+
const proxyUsed = Boolean(error.proxyUsed);
|
|
666
370
|
if (attempt < retryOptions.attempts &&
|
|
667
|
-
|
|
668
|
-
|
|
371
|
+
shouldRetryProxyTransportAttempt({
|
|
372
|
+
error,
|
|
373
|
+
explicitRetry,
|
|
374
|
+
method: methodName,
|
|
375
|
+
options: retryOptions,
|
|
376
|
+
proxyUsed,
|
|
377
|
+
})) {
|
|
378
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
669
379
|
continue;
|
|
670
380
|
}
|
|
671
381
|
throw error;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types";
|
|
2
|
+
import { HttpRetryPreset } from "../types";
|
|
3
|
+
export type NormalizedProxyTransportRetryOptions = Required<Pick<HttpRetryOptions, "attempts" | "delayStrategy" | "baseDelayMs" | "maxDelayMs" | "jitter" | "retryAfter" | "unsafeMethodPolicy">> & {
|
|
4
|
+
preset?: HttpRetryPreset;
|
|
5
|
+
methods: readonly string[];
|
|
6
|
+
statusCodes: readonly number[];
|
|
7
|
+
errorCodes: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS: readonly ["GET", "HEAD", "OPTIONS"];
|
|
10
|
+
export declare const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES: readonly ["transport_network_error", "transport_timeout"];
|
|
11
|
+
export declare const MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS = 8;
|
|
12
|
+
type RetryPolicyLabel = "HTTP" | "Stealth" | "Proxy transport";
|
|
13
|
+
export declare function proxyTransportRetryErrorCode(error: unknown): string | undefined;
|
|
14
|
+
export declare function proxyTransportRetryErrorStatus(error: unknown): number | undefined;
|
|
15
|
+
export declare function createDefaultProxyTransportRetryOptions(options?: {
|
|
16
|
+
extraErrorCodes?: readonly string[];
|
|
17
|
+
label?: RetryPolicyLabel;
|
|
18
|
+
}): NormalizedProxyTransportRetryOptions;
|
|
19
|
+
export declare function normalizeProxyTransportRetryOptions(retry: RequestOptions["retry"], options?: {
|
|
20
|
+
extraErrorCodes?: readonly string[];
|
|
21
|
+
label?: RetryPolicyLabel;
|
|
22
|
+
}): NormalizedProxyTransportRetryOptions | undefined;
|
|
23
|
+
export declare function validateUnsafeProxyTransportRetryMethods(options: NormalizedProxyTransportRetryOptions, label?: RetryPolicyLabel): void;
|
|
24
|
+
export declare function isProxyTransportRetryMethod(method: HttpMethod | string, options: NormalizedProxyTransportRetryOptions): boolean;
|
|
25
|
+
export declare function shouldRetryProxyTransportError(error: unknown, options: NormalizedProxyTransportRetryOptions): boolean;
|
|
26
|
+
export declare function shouldRetryProxyTransportAttempt(input: {
|
|
27
|
+
error: unknown;
|
|
28
|
+
explicitRetry: boolean;
|
|
29
|
+
method: HttpMethod | string;
|
|
30
|
+
options: NormalizedProxyTransportRetryOptions | undefined;
|
|
31
|
+
proxyUsed: boolean;
|
|
32
|
+
}): boolean;
|
|
33
|
+
export declare function computeProxyTransportRetryDelayMs(options: NormalizedProxyTransportRetryOptions, attemptIndex: number, headers?: Record<string, string>): number;
|
|
34
|
+
export declare function normalizeProxyAttemptIndex(value: number | undefined): number;
|
|
35
|
+
export declare function computeProxyAttemptIndex(options: {
|
|
36
|
+
baseProxyAttempt?: number;
|
|
37
|
+
proxyAttemptOffset?: number;
|
|
38
|
+
retryAttemptOffset?: number;
|
|
39
|
+
}): number;
|
|
40
|
+
export {};
|