@apifuse/provider-sdk 2.2.0-beta.1 → 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/CHANGELOG.md +5 -0
- package/dist/runtime/http.js +28 -338
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.js +57 -203
- package/dist/server/serve.js +5 -2
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/package.json +1 -1
- package/src/runtime/http.ts +60 -547
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +100 -361
- package/src/server/serve.ts +8 -2
- package/src/server/types.ts +1 -0
package/src/runtime/http.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import type { ProxyResolutionOptions } from "../config/loader";
|
|
2
2
|
import { resolveProxyConfigAsync } from "../config/loader";
|
|
3
3
|
import { ProviderError, TransportError } from "../errors";
|
|
4
|
-
import {
|
|
5
|
-
parseSseStream,
|
|
6
|
-
readableBytes,
|
|
7
|
-
readableLines,
|
|
8
|
-
readableTextChunks,
|
|
9
|
-
} from "../stream";
|
|
4
|
+
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream";
|
|
10
5
|
import type {
|
|
11
6
|
HttpClient,
|
|
12
7
|
HttpMethod,
|
|
13
8
|
HttpResponse,
|
|
14
|
-
HttpRetryOptions,
|
|
15
9
|
HttpRetrySummary,
|
|
16
10
|
HttpStreamResponse,
|
|
17
11
|
RequestOptions,
|
|
@@ -19,12 +13,16 @@ import type {
|
|
|
19
13
|
SseMessage,
|
|
20
14
|
} from "../types";
|
|
21
15
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
computeProxyAttemptIndex,
|
|
17
|
+
computeProxyTransportRetryDelayMs,
|
|
18
|
+
createDefaultProxyTransportRetryOptions,
|
|
19
|
+
isProxyTransportRetryMethod,
|
|
20
|
+
normalizeProxyTransportRetryOptions,
|
|
21
|
+
proxyTransportRetryErrorCode,
|
|
22
|
+
proxyTransportRetryErrorStatus,
|
|
23
|
+
shouldRetryProxyTransportAttempt,
|
|
24
|
+
validateUnsafeProxyTransportRetryMethods,
|
|
25
|
+
} from "./proxy-retry-policy";
|
|
28
26
|
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
|
|
29
27
|
|
|
30
28
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
@@ -35,24 +33,6 @@ export type HttpClientOptions = ProxyResolutionOptions & {
|
|
|
35
33
|
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
36
34
|
};
|
|
37
35
|
|
|
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
36
|
type HttpStatusOutcome = {
|
|
57
37
|
kind: "http-status";
|
|
58
38
|
status: number;
|
|
@@ -71,416 +51,6 @@ function isHttpStatusOutcome(
|
|
|
71
51
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"] as const;
|
|
75
|
-
const DEFAULT_RETRY_ERROR_CODES = [
|
|
76
|
-
"transport_network_error",
|
|
77
|
-
"transport_timeout",
|
|
78
|
-
] as const;
|
|
79
|
-
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
|
|
80
|
-
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503] as const;
|
|
81
|
-
const KNOWN_RETRY_METHODS = new Set([
|
|
82
|
-
"GET",
|
|
83
|
-
"HEAD",
|
|
84
|
-
"POST",
|
|
85
|
-
"PUT",
|
|
86
|
-
"DELETE",
|
|
87
|
-
"OPTIONS",
|
|
88
|
-
"TRACE",
|
|
89
|
-
"PATCH",
|
|
90
|
-
]);
|
|
91
|
-
const UNSAFE_RETRY_METHODS = new Set([
|
|
92
|
-
"POST",
|
|
93
|
-
"PUT",
|
|
94
|
-
"PATCH",
|
|
95
|
-
"DELETE",
|
|
96
|
-
"TRACE",
|
|
97
|
-
]);
|
|
98
|
-
const MAX_RETRY_ATTEMPTS = 8;
|
|
99
|
-
const MAX_RETRY_DELAY_MS = 30_000;
|
|
100
|
-
|
|
101
|
-
function hasOwnValue<T extends string>(
|
|
102
|
-
values: Record<string, T>,
|
|
103
|
-
value: unknown,
|
|
104
|
-
): value is T {
|
|
105
|
-
if (typeof value !== "string") return false;
|
|
106
|
-
return Object.values(values).some((candidate) => candidate === value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function createInvalidRetryPolicyError(message: string): ProviderError {
|
|
110
|
-
return new ProviderError(message, { code: "retry_invalid_policy" });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function createRetryOptions(preset: HttpRetryPreset): NormalizedRetryOptions {
|
|
114
|
-
switch (preset) {
|
|
115
|
-
case HttpRetryPreset.Off:
|
|
116
|
-
return {
|
|
117
|
-
preset,
|
|
118
|
-
attempts: 1,
|
|
119
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
120
|
-
statusCodes: [],
|
|
121
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
122
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
123
|
-
baseDelayMs: 100,
|
|
124
|
-
maxDelayMs: 1_000,
|
|
125
|
-
jitter: HttpRetryJitter.Full,
|
|
126
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
127
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
128
|
-
};
|
|
129
|
-
case HttpRetryPreset.SafeRead:
|
|
130
|
-
return {
|
|
131
|
-
preset,
|
|
132
|
-
attempts: 3,
|
|
133
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
134
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
135
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
136
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
137
|
-
baseDelayMs: 100,
|
|
138
|
-
maxDelayMs: 2_000,
|
|
139
|
-
jitter: HttpRetryJitter.Full,
|
|
140
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
141
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
142
|
-
};
|
|
143
|
-
case HttpRetryPreset.AggressiveRead:
|
|
144
|
-
return {
|
|
145
|
-
preset,
|
|
146
|
-
attempts: 4,
|
|
147
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
148
|
-
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
149
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
150
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
151
|
-
baseDelayMs: 150,
|
|
152
|
-
maxDelayMs: 5_000,
|
|
153
|
-
jitter: HttpRetryJitter.Full,
|
|
154
|
-
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
155
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
156
|
-
};
|
|
157
|
-
case HttpRetryPreset.RateLimitAware:
|
|
158
|
-
return {
|
|
159
|
-
preset,
|
|
160
|
-
attempts: 3,
|
|
161
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
162
|
-
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
163
|
-
errorCodes: ["transport_timeout"],
|
|
164
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
165
|
-
baseDelayMs: 250,
|
|
166
|
-
maxDelayMs: 5_000,
|
|
167
|
-
jitter: HttpRetryJitter.Equal,
|
|
168
|
-
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
169
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
170
|
-
};
|
|
171
|
-
case HttpRetryPreset.TransportTransient:
|
|
172
|
-
return {
|
|
173
|
-
preset,
|
|
174
|
-
attempts: 3,
|
|
175
|
-
methods: DEFAULT_RETRY_METHODS,
|
|
176
|
-
statusCodes: [],
|
|
177
|
-
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
178
|
-
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
179
|
-
baseDelayMs: 100,
|
|
180
|
-
maxDelayMs: 1_000,
|
|
181
|
-
jitter: HttpRetryJitter.Full,
|
|
182
|
-
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
183
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function clampPositiveInteger(
|
|
190
|
-
value: number | undefined,
|
|
191
|
-
fallback: number,
|
|
192
|
-
max: number,
|
|
193
|
-
): number {
|
|
194
|
-
if (value === undefined) return fallback;
|
|
195
|
-
if (!Number.isFinite(value) || value < 1) return fallback;
|
|
196
|
-
return Math.min(Math.floor(value), max);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function clampDelay(value: number | undefined, fallback: number): number {
|
|
200
|
-
if (value === undefined) return fallback;
|
|
201
|
-
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
202
|
-
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function normalizeRetryOptions(
|
|
206
|
-
retry: RequestOptions["retry"],
|
|
207
|
-
): NormalizedRetryOptions | undefined {
|
|
208
|
-
if (retry === undefined || retry === false) {
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
if (retry === true) {
|
|
212
|
-
return createRetryOptions(HttpRetryPreset.TransportTransient);
|
|
213
|
-
}
|
|
214
|
-
if (typeof retry === "string") {
|
|
215
|
-
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
216
|
-
throw createInvalidRetryPolicyError(
|
|
217
|
-
`Unknown HTTP retry preset: ${retry}`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
return createRetryOptions(retry);
|
|
221
|
-
}
|
|
222
|
-
if (typeof retry !== "object" || retry === null) {
|
|
223
|
-
throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
|
|
224
|
-
}
|
|
225
|
-
if (Array.isArray(retry)) {
|
|
226
|
-
throw createInvalidRetryPolicyError(
|
|
227
|
-
"HTTP retry policy must be a plain object",
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
validateRetryOptionsShape(retry);
|
|
232
|
-
const base = createRetryOptions(
|
|
233
|
-
retry.preset ?? HttpRetryPreset.TransportTransient,
|
|
234
|
-
);
|
|
235
|
-
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
236
|
-
const normalized: NormalizedRetryOptions = {
|
|
237
|
-
preset: retry.preset ?? base.preset,
|
|
238
|
-
attempts: clampPositiveInteger(
|
|
239
|
-
retry.attempts,
|
|
240
|
-
base.attempts,
|
|
241
|
-
MAX_RETRY_ATTEMPTS,
|
|
242
|
-
),
|
|
243
|
-
methods:
|
|
244
|
-
retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
245
|
-
statusCodes:
|
|
246
|
-
retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
|
|
247
|
-
base.statusCodes,
|
|
248
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
249
|
-
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
250
|
-
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
251
|
-
maxDelayMs,
|
|
252
|
-
jitter: retry.jitter ?? base.jitter,
|
|
253
|
-
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
254
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
return normalized;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function validateRetryOptionsShape(retry: HttpRetryOptions): void {
|
|
261
|
-
if (
|
|
262
|
-
retry.preset !== undefined &&
|
|
263
|
-
!hasOwnValue(HttpRetryPreset, retry.preset)
|
|
264
|
-
) {
|
|
265
|
-
throw createInvalidRetryPolicyError(
|
|
266
|
-
`Unknown HTTP retry preset: ${String(retry.preset)}`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (
|
|
270
|
-
retry.delayStrategy !== undefined &&
|
|
271
|
-
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)
|
|
272
|
-
) {
|
|
273
|
-
throw createInvalidRetryPolicyError(
|
|
274
|
-
`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (
|
|
278
|
-
retry.jitter !== undefined &&
|
|
279
|
-
!hasOwnValue(HttpRetryJitter, retry.jitter)
|
|
280
|
-
) {
|
|
281
|
-
throw createInvalidRetryPolicyError(
|
|
282
|
-
`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
if (
|
|
286
|
-
retry.retryAfter !== undefined &&
|
|
287
|
-
!hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)
|
|
288
|
-
) {
|
|
289
|
-
throw createInvalidRetryPolicyError(
|
|
290
|
-
`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
if (
|
|
294
|
-
retry.unsafeMethodPolicy !== undefined &&
|
|
295
|
-
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)
|
|
296
|
-
) {
|
|
297
|
-
throw createInvalidRetryPolicyError(
|
|
298
|
-
`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
if (retry.methods !== undefined) {
|
|
302
|
-
if (!Array.isArray(retry.methods)) {
|
|
303
|
-
throw createInvalidRetryPolicyError(
|
|
304
|
-
"HTTP retry methods must be an array",
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
const nonStringMethods = retry.methods.filter(
|
|
308
|
-
(method) => typeof method !== "string",
|
|
309
|
-
);
|
|
310
|
-
if (nonStringMethods.length > 0) {
|
|
311
|
-
throw createInvalidRetryPolicyError(
|
|
312
|
-
"HTTP retry methods must contain only strings",
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
const unknownMethods = retry.methods
|
|
316
|
-
.map((method) => method.toUpperCase())
|
|
317
|
-
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
318
|
-
if (unknownMethods.length > 0) {
|
|
319
|
-
throw createInvalidRetryPolicyError(
|
|
320
|
-
`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (retry.statusCodes !== undefined) {
|
|
325
|
-
if (!Array.isArray(retry.statusCodes)) {
|
|
326
|
-
throw createInvalidRetryPolicyError(
|
|
327
|
-
"HTTP retry statusCodes must be an array",
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
const invalidStatusCodes = retry.statusCodes.filter(
|
|
331
|
-
(status) =>
|
|
332
|
-
!Number.isInteger(status) ||
|
|
333
|
-
Number(status) < 100 ||
|
|
334
|
-
Number(status) > 599,
|
|
335
|
-
);
|
|
336
|
-
if (invalidStatusCodes.length > 0) {
|
|
337
|
-
throw createInvalidRetryPolicyError(
|
|
338
|
-
"HTTP retry statusCodes must contain HTTP status integers in [100, 599]",
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (retry.errorCodes !== undefined) {
|
|
343
|
-
if (!Array.isArray(retry.errorCodes)) {
|
|
344
|
-
throw createInvalidRetryPolicyError(
|
|
345
|
-
"HTTP retry errorCodes must be an array",
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const nonStringErrorCodes = retry.errorCodes.filter(
|
|
349
|
-
(errorCode) => typeof errorCode !== "string",
|
|
350
|
-
);
|
|
351
|
-
if (nonStringErrorCodes.length > 0) {
|
|
352
|
-
throw createInvalidRetryPolicyError(
|
|
353
|
-
"HTTP retry errorCodes must contain only strings",
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (
|
|
358
|
-
retry.preset === HttpRetryPreset.Off &&
|
|
359
|
-
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
360
|
-
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))
|
|
361
|
-
) {
|
|
362
|
-
throw createInvalidRetryPolicyError(
|
|
363
|
-
"HTTP retry preset off cannot be combined with retry-enabling overrides",
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function validateUnsafeRetryMethods(options: NormalizedRetryOptions): void {
|
|
369
|
-
if (
|
|
370
|
-
options.unsafeMethodPolicy ===
|
|
371
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
|
|
372
|
-
) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const unsafeMethods = options.methods.filter((method) =>
|
|
376
|
-
UNSAFE_RETRY_METHODS.has(method.toUpperCase()),
|
|
377
|
-
);
|
|
378
|
-
if (unsafeMethods.length === 0) return;
|
|
379
|
-
|
|
380
|
-
throw new ProviderError(
|
|
381
|
-
`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
|
|
382
|
-
{ code: "retry_unsafe_method" },
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function isMethodRetryable(
|
|
387
|
-
method: HttpMethod,
|
|
388
|
-
options: NormalizedRetryOptions,
|
|
389
|
-
): boolean {
|
|
390
|
-
return options.methods
|
|
391
|
-
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
392
|
-
.includes(method.toUpperCase());
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function retryErrorCode(error: unknown): string | undefined {
|
|
396
|
-
if (error instanceof TransportError) {
|
|
397
|
-
return error.code;
|
|
398
|
-
}
|
|
399
|
-
if (error && typeof error === "object" && "code" in error) {
|
|
400
|
-
const code = Reflect.get(error, "code");
|
|
401
|
-
return typeof code === "string" ? code : undefined;
|
|
402
|
-
}
|
|
403
|
-
return undefined;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function retryErrorStatus(error: unknown): number | undefined {
|
|
407
|
-
if (error instanceof TransportError) {
|
|
408
|
-
return error.status ?? error.upstreamStatus;
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function shouldRetryTransportError(
|
|
414
|
-
error: unknown,
|
|
415
|
-
options: NormalizedRetryOptions,
|
|
416
|
-
): boolean {
|
|
417
|
-
const code = retryErrorCode(error);
|
|
418
|
-
return Boolean(code && options.errorCodes.includes(code));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function retryAfterHeader(headers: Record<string, string>): string | undefined {
|
|
422
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
423
|
-
if (name.toLowerCase() === "retry-after") return value;
|
|
424
|
-
}
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function parseRetryAfterMs(
|
|
429
|
-
headers: Record<string, string>,
|
|
430
|
-
now: number = Date.now(),
|
|
431
|
-
): number | undefined {
|
|
432
|
-
const value = retryAfterHeader(headers);
|
|
433
|
-
if (!value) return undefined;
|
|
434
|
-
const seconds = Number(value);
|
|
435
|
-
if (Number.isFinite(seconds)) {
|
|
436
|
-
return Math.max(0, Math.floor(seconds * 1_000));
|
|
437
|
-
}
|
|
438
|
-
const dateMs = Date.parse(value);
|
|
439
|
-
if (!Number.isNaN(dateMs)) {
|
|
440
|
-
return Math.max(0, dateMs - now);
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function computeRetryDelayMs(
|
|
446
|
-
options: NormalizedRetryOptions,
|
|
447
|
-
attemptIndex: number,
|
|
448
|
-
headers?: Record<string, string>,
|
|
449
|
-
): number {
|
|
450
|
-
const multiplier =
|
|
451
|
-
options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
452
|
-
? 2 ** Math.max(0, attemptIndex - 1)
|
|
453
|
-
: 1;
|
|
454
|
-
const configuredDelay = Math.min(
|
|
455
|
-
options.baseDelayMs * multiplier,
|
|
456
|
-
options.maxDelayMs,
|
|
457
|
-
);
|
|
458
|
-
const retryAfterMs =
|
|
459
|
-
options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
460
|
-
? undefined
|
|
461
|
-
: headers
|
|
462
|
-
? parseRetryAfterMs(headers)
|
|
463
|
-
: undefined;
|
|
464
|
-
if (retryAfterMs !== undefined) {
|
|
465
|
-
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
466
|
-
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
467
|
-
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
468
|
-
}
|
|
469
|
-
return boundedRetryAfterMs;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
switch (options.jitter) {
|
|
473
|
-
case HttpRetryJitter.None:
|
|
474
|
-
return configuredDelay;
|
|
475
|
-
case HttpRetryJitter.Equal:
|
|
476
|
-
return Math.floor(
|
|
477
|
-
configuredDelay / 2 + Math.random() * (configuredDelay / 2),
|
|
478
|
-
);
|
|
479
|
-
case HttpRetryJitter.Full:
|
|
480
|
-
return Math.floor(Math.random() * configuredDelay);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
54
|
async function sleep(ms: number): Promise<void> {
|
|
485
55
|
if (ms <= 0) return;
|
|
486
56
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -505,9 +75,7 @@ function withClientHeaders(
|
|
|
505
75
|
body: unknown,
|
|
506
76
|
): RequestOptions {
|
|
507
77
|
const headers: Record<string, string> = {
|
|
508
|
-
...(clientOptions.userAgent
|
|
509
|
-
? { "User-Agent": clientOptions.userAgent }
|
|
510
|
-
: {}),
|
|
78
|
+
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
511
79
|
...options?.headers,
|
|
512
80
|
};
|
|
513
81
|
|
|
@@ -522,10 +90,7 @@ function withClientHeaders(
|
|
|
522
90
|
}
|
|
523
91
|
|
|
524
92
|
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
525
|
-
const contentType =
|
|
526
|
-
headers["content-type"] ??
|
|
527
|
-
headers["Content-Type"] ??
|
|
528
|
-
headers["CONTENT-TYPE"];
|
|
93
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
529
94
|
|
|
530
95
|
if (contentType?.includes("application/json")) {
|
|
531
96
|
return body ? JSON.parse(body) : null;
|
|
@@ -596,23 +161,14 @@ async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
|
596
161
|
headers,
|
|
597
162
|
json: async <T = unknown>() => {
|
|
598
163
|
const contentType =
|
|
599
|
-
headers["content-type"] ??
|
|
600
|
-
|
|
601
|
-
headers["CONTENT-TYPE"];
|
|
602
|
-
return parseJson<T>(
|
|
603
|
-
contentType?.includes("application/json") && !rawText
|
|
604
|
-
? "null"
|
|
605
|
-
: rawText,
|
|
606
|
-
);
|
|
164
|
+
headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
165
|
+
return parseJson<T>(contentType?.includes("application/json") && !rawText ? "null" : rawText);
|
|
607
166
|
},
|
|
608
167
|
ok: response.status >= 200 && response.status < 300,
|
|
609
168
|
status: response.status,
|
|
610
169
|
text: async () => rawText,
|
|
611
170
|
arrayBuffer: async () =>
|
|
612
|
-
bodyBytes.buffer.slice(
|
|
613
|
-
bodyBytes.byteOffset,
|
|
614
|
-
bodyBytes.byteOffset + bodyBytes.byteLength,
|
|
615
|
-
),
|
|
171
|
+
bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
616
172
|
bytes: async () => bodyBytes.slice(0),
|
|
617
173
|
};
|
|
618
174
|
}
|
|
@@ -625,9 +181,7 @@ async function drainNativeResponseBody(response: Response): Promise<void> {
|
|
|
625
181
|
}
|
|
626
182
|
}
|
|
627
183
|
|
|
628
|
-
function requireNativeResponseBody(
|
|
629
|
-
response: Response,
|
|
630
|
-
): ReadableStream<Uint8Array> {
|
|
184
|
+
function requireNativeResponseBody(response: Response): ReadableStream<Uint8Array> {
|
|
631
185
|
if (!response.body) {
|
|
632
186
|
throw new TransportError("Response body stream is unavailable", {
|
|
633
187
|
code: "transport_stream_unavailable",
|
|
@@ -692,18 +246,16 @@ async function resolveNativeProxy(
|
|
|
692
246
|
warn: (message: string) => void,
|
|
693
247
|
proxyAttemptOffset = 0,
|
|
694
248
|
): Promise<string | undefined> {
|
|
695
|
-
const baseProxyAttempt =
|
|
696
|
-
clientOptions.proxyAttempt === undefined ||
|
|
697
|
-
!Number.isFinite(clientOptions.proxyAttempt)
|
|
698
|
-
? 0
|
|
699
|
-
: Math.max(0, Math.floor(clientOptions.proxyAttempt));
|
|
700
249
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
701
250
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
702
251
|
upstream: clientOptions.upstream,
|
|
703
252
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
704
253
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
705
254
|
affinityKey: clientOptions.affinityKey,
|
|
706
|
-
proxyAttempt:
|
|
255
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
256
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
257
|
+
retryAttemptOffset: proxyAttemptOffset,
|
|
258
|
+
}),
|
|
707
259
|
telemetry: clientOptions.telemetry,
|
|
708
260
|
});
|
|
709
261
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -723,9 +275,7 @@ function assertNoHttpTransportOverrides(options: RequestOptions): void {
|
|
|
723
275
|
}
|
|
724
276
|
}
|
|
725
277
|
|
|
726
|
-
function normalizeNativeFetchBody(
|
|
727
|
-
body: unknown,
|
|
728
|
-
): string | ArrayBuffer | undefined {
|
|
278
|
+
function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefined {
|
|
729
279
|
const normalized = normalizeHttpRequestBody(body);
|
|
730
280
|
if (!Buffer.isBuffer(normalized)) {
|
|
731
281
|
return normalized;
|
|
@@ -745,10 +295,7 @@ async function fetchNativeHttp(
|
|
|
745
295
|
statusRetryCodes?: readonly number[],
|
|
746
296
|
proxyAttemptOffset = 0,
|
|
747
297
|
): Promise<NativeHttpAttemptOutcome> {
|
|
748
|
-
const requestUrl = appendQueryParams(
|
|
749
|
-
resolveHttpUrl(baseUrl, url),
|
|
750
|
-
options.params,
|
|
751
|
-
);
|
|
298
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
752
299
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
753
300
|
const timeoutHandle = options.timeout
|
|
754
301
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -756,12 +303,7 @@ async function fetchNativeHttp(
|
|
|
756
303
|
|
|
757
304
|
let proxy: string | undefined;
|
|
758
305
|
try {
|
|
759
|
-
proxy = await resolveNativeProxy(
|
|
760
|
-
options,
|
|
761
|
-
clientOptions,
|
|
762
|
-
warn,
|
|
763
|
-
proxyAttemptOffset,
|
|
764
|
-
);
|
|
306
|
+
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
765
307
|
const requestInit: NativeFetchInit = {
|
|
766
308
|
headers: options.headers,
|
|
767
309
|
method,
|
|
@@ -789,13 +331,10 @@ async function fetchNativeHttp(
|
|
|
789
331
|
|
|
790
332
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
791
333
|
await drainNativeResponseBody(response);
|
|
792
|
-
throw new TransportError(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
status: response.status,
|
|
797
|
-
},
|
|
798
|
-
);
|
|
334
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
335
|
+
code: "upstream_http_error",
|
|
336
|
+
status: response.status,
|
|
337
|
+
});
|
|
799
338
|
}
|
|
800
339
|
|
|
801
340
|
return toNativeHttpResponse(response);
|
|
@@ -819,10 +358,7 @@ async function fetchNativeHttpStream(
|
|
|
819
358
|
clientOptions: HttpClientOptions,
|
|
820
359
|
warn: (message: string) => void,
|
|
821
360
|
): Promise<HttpStreamResponse> {
|
|
822
|
-
const requestUrl = appendQueryParams(
|
|
823
|
-
resolveHttpUrl(baseUrl, url),
|
|
824
|
-
options.params,
|
|
825
|
-
);
|
|
361
|
+
const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
|
|
826
362
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
827
363
|
const timeoutHandle = options.timeout
|
|
828
364
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -845,13 +381,10 @@ async function fetchNativeHttpStream(
|
|
|
845
381
|
|
|
846
382
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
847
383
|
await drainNativeResponseBody(response);
|
|
848
|
-
throw new TransportError(
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
status: response.status,
|
|
853
|
-
},
|
|
854
|
-
);
|
|
384
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
385
|
+
code: "upstream_http_error",
|
|
386
|
+
status: response.status,
|
|
387
|
+
});
|
|
855
388
|
}
|
|
856
389
|
|
|
857
390
|
return toNativeHttpStreamResponse(response);
|
|
@@ -891,23 +424,19 @@ export function createHttpClient(
|
|
|
891
424
|
);
|
|
892
425
|
}
|
|
893
426
|
assertNoHttpTransportOverrides(options);
|
|
894
|
-
const headersOptions = withClientHeaders(
|
|
895
|
-
options,
|
|
896
|
-
clientOptions,
|
|
897
|
-
options.body,
|
|
898
|
-
);
|
|
427
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
899
428
|
const methodName = normalizeHttpMethod(method);
|
|
900
429
|
const explicitRetry = headersOptions.retry !== undefined;
|
|
901
430
|
const retryOptions =
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (retryOptions)
|
|
431
|
+
normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
432
|
+
label: "HTTP",
|
|
433
|
+
}) ??
|
|
434
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
435
|
+
if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
907
436
|
const retryEnabled = Boolean(
|
|
908
437
|
retryOptions &&
|
|
909
438
|
retryOptions.attempts > 1 &&
|
|
910
|
-
|
|
439
|
+
isProxyTransportRetryMethod(methodName, retryOptions),
|
|
911
440
|
);
|
|
912
441
|
const statusRetryEnabled = Boolean(
|
|
913
442
|
retryEnabled &&
|
|
@@ -916,14 +445,11 @@ export function createHttpClient(
|
|
|
916
445
|
retryOptions.statusCodes.length > 0 &&
|
|
917
446
|
headersOptions.throwOnHttpError !== false,
|
|
918
447
|
);
|
|
919
|
-
const attemptOptions: RequestOptions & { body?: unknown } =
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
: headersOptions;
|
|
448
|
+
const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
|
|
449
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
450
|
+
: headersOptions;
|
|
923
451
|
|
|
924
|
-
const executeOnce = (
|
|
925
|
-
proxyAttemptOffset = 0,
|
|
926
|
-
): Promise<NativeHttpAttemptOutcome> =>
|
|
452
|
+
const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
|
|
927
453
|
fetchNativeHttp(
|
|
928
454
|
baseUrl,
|
|
929
455
|
url,
|
|
@@ -951,19 +477,14 @@ export function createHttpClient(
|
|
|
951
477
|
if (isHttpStatusOutcome(outcome)) {
|
|
952
478
|
lastStatus = outcome.status;
|
|
953
479
|
if (outcome.retryable && attempt < retryOptions.attempts) {
|
|
954
|
-
await sleep(
|
|
955
|
-
computeRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
956
|
-
);
|
|
480
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
957
481
|
continue;
|
|
958
482
|
}
|
|
959
483
|
throw toUpstreamHttpError(outcome.status);
|
|
960
484
|
}
|
|
961
485
|
|
|
962
486
|
const response = outcome;
|
|
963
|
-
if (
|
|
964
|
-
response.status >= 400 &&
|
|
965
|
-
headersOptions.throwOnHttpError !== false
|
|
966
|
-
) {
|
|
487
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
967
488
|
throw toUpstreamHttpError(response.status);
|
|
968
489
|
}
|
|
969
490
|
|
|
@@ -980,15 +501,20 @@ export function createHttpClient(
|
|
|
980
501
|
}
|
|
981
502
|
return response;
|
|
982
503
|
} catch (error) {
|
|
983
|
-
lastErrorCode =
|
|
984
|
-
lastStatus =
|
|
504
|
+
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
505
|
+
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
985
506
|
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
986
507
|
if (
|
|
987
508
|
attempt < retryOptions.attempts &&
|
|
988
|
-
(
|
|
989
|
-
|
|
509
|
+
shouldRetryProxyTransportAttempt({
|
|
510
|
+
error,
|
|
511
|
+
explicitRetry,
|
|
512
|
+
method: methodName,
|
|
513
|
+
options: retryOptions,
|
|
514
|
+
proxyUsed,
|
|
515
|
+
})
|
|
990
516
|
) {
|
|
991
|
-
await sleep(
|
|
517
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
992
518
|
continue;
|
|
993
519
|
}
|
|
994
520
|
throw error;
|
|
@@ -1012,30 +538,17 @@ export function createHttpClient(
|
|
|
1012
538
|
);
|
|
1013
539
|
}
|
|
1014
540
|
assertNoHttpTransportOverrides(options);
|
|
1015
|
-
const headersOptions = withClientHeaders(
|
|
1016
|
-
options,
|
|
1017
|
-
clientOptions,
|
|
1018
|
-
options.body,
|
|
1019
|
-
);
|
|
541
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
1020
542
|
const methodName = normalizeHttpMethod(method);
|
|
1021
|
-
return fetchNativeHttpStream(
|
|
1022
|
-
baseUrl,
|
|
1023
|
-
url,
|
|
1024
|
-
methodName,
|
|
1025
|
-
headersOptions,
|
|
1026
|
-
clientOptions,
|
|
1027
|
-
warnOnce,
|
|
1028
|
-
);
|
|
543
|
+
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
1029
544
|
}
|
|
1030
545
|
|
|
1031
546
|
return {
|
|
1032
547
|
request: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1033
548
|
request(url, options.method ?? "GET", options),
|
|
1034
549
|
get: async (url, options) => request(url, "GET", options),
|
|
1035
|
-
post: async (url, body, options) =>
|
|
1036
|
-
|
|
1037
|
-
put: async (url, body, options) =>
|
|
1038
|
-
request(url, "PUT", { ...options, body }),
|
|
550
|
+
post: async (url, body, options) => request(url, "POST", { ...options, body }),
|
|
551
|
+
put: async (url, body, options) => request(url, "PUT", { ...options, body }),
|
|
1039
552
|
delete: async (url, options) => request(url, "DELETE", options),
|
|
1040
553
|
stream: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1041
554
|
streamRequest(url, options.method ?? "GET", options),
|