@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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { ProviderError, TransportError } from "../errors";
|
|
2
|
+
import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types";
|
|
3
|
+
export const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
4
|
+
export const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES = [
|
|
5
|
+
"transport_network_error",
|
|
6
|
+
"transport_timeout",
|
|
7
|
+
];
|
|
8
|
+
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
9
|
+
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503];
|
|
10
|
+
const RATE_LIMIT_RETRY_ERROR_CODES = ["transport_timeout"];
|
|
11
|
+
const KNOWN_RETRY_METHODS = new Set([
|
|
12
|
+
"GET",
|
|
13
|
+
"HEAD",
|
|
14
|
+
"POST",
|
|
15
|
+
"PUT",
|
|
16
|
+
"DELETE",
|
|
17
|
+
"OPTIONS",
|
|
18
|
+
"TRACE",
|
|
19
|
+
"PATCH",
|
|
20
|
+
]);
|
|
21
|
+
const UNSAFE_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
|
|
22
|
+
export const MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS = 8;
|
|
23
|
+
const MAX_RETRY_DELAY_MS = 30_000;
|
|
24
|
+
function hasOwnValue(values, value) {
|
|
25
|
+
if (typeof value !== "string")
|
|
26
|
+
return false;
|
|
27
|
+
return Object.values(values).some((candidate) => candidate === value);
|
|
28
|
+
}
|
|
29
|
+
function createInvalidRetryPolicyError(message, label) {
|
|
30
|
+
return new ProviderError(message.replace("{{label}}", label), {
|
|
31
|
+
code: "retry_invalid_policy",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function retryErrorCode(error) {
|
|
35
|
+
if (error instanceof TransportError) {
|
|
36
|
+
return error.code;
|
|
37
|
+
}
|
|
38
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
39
|
+
const code = Reflect.get(error, "code");
|
|
40
|
+
return typeof code === "string" ? code : undefined;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function retryErrorStatus(error) {
|
|
45
|
+
if (error instanceof TransportError) {
|
|
46
|
+
return error.status ?? error.upstreamStatus;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export function proxyTransportRetryErrorCode(error) {
|
|
51
|
+
return retryErrorCode(error);
|
|
52
|
+
}
|
|
53
|
+
export function proxyTransportRetryErrorStatus(error) {
|
|
54
|
+
return retryErrorStatus(error);
|
|
55
|
+
}
|
|
56
|
+
function createRetryOptions(preset, extraErrorCodes, label) {
|
|
57
|
+
const defaultErrorCodes = [...DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES, ...extraErrorCodes];
|
|
58
|
+
switch (preset) {
|
|
59
|
+
case HttpRetryPreset.Off:
|
|
60
|
+
return {
|
|
61
|
+
preset,
|
|
62
|
+
attempts: 1,
|
|
63
|
+
methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
|
|
64
|
+
statusCodes: [],
|
|
65
|
+
errorCodes: defaultErrorCodes,
|
|
66
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
67
|
+
baseDelayMs: 100,
|
|
68
|
+
maxDelayMs: 1_000,
|
|
69
|
+
jitter: HttpRetryJitter.Full,
|
|
70
|
+
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
71
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
72
|
+
};
|
|
73
|
+
case HttpRetryPreset.SafeRead:
|
|
74
|
+
return {
|
|
75
|
+
preset,
|
|
76
|
+
attempts: 3,
|
|
77
|
+
methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
|
|
78
|
+
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
79
|
+
errorCodes: defaultErrorCodes,
|
|
80
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
81
|
+
baseDelayMs: 100,
|
|
82
|
+
maxDelayMs: 2_000,
|
|
83
|
+
jitter: HttpRetryJitter.Full,
|
|
84
|
+
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
85
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
86
|
+
};
|
|
87
|
+
case HttpRetryPreset.AggressiveRead:
|
|
88
|
+
return {
|
|
89
|
+
preset,
|
|
90
|
+
attempts: 4,
|
|
91
|
+
methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
|
|
92
|
+
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
93
|
+
errorCodes: defaultErrorCodes,
|
|
94
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
95
|
+
baseDelayMs: 150,
|
|
96
|
+
maxDelayMs: 5_000,
|
|
97
|
+
jitter: HttpRetryJitter.Full,
|
|
98
|
+
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
99
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
100
|
+
};
|
|
101
|
+
case HttpRetryPreset.RateLimitAware:
|
|
102
|
+
return {
|
|
103
|
+
preset,
|
|
104
|
+
attempts: 3,
|
|
105
|
+
methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
|
|
106
|
+
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
107
|
+
errorCodes: RATE_LIMIT_RETRY_ERROR_CODES,
|
|
108
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
109
|
+
baseDelayMs: 250,
|
|
110
|
+
maxDelayMs: 5_000,
|
|
111
|
+
jitter: HttpRetryJitter.Equal,
|
|
112
|
+
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
113
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
114
|
+
};
|
|
115
|
+
case HttpRetryPreset.TransportTransient:
|
|
116
|
+
return {
|
|
117
|
+
preset,
|
|
118
|
+
attempts: 3,
|
|
119
|
+
methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
|
|
120
|
+
statusCodes: [],
|
|
121
|
+
errorCodes: defaultErrorCodes,
|
|
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
|
+
}
|
|
130
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${preset}`, label);
|
|
131
|
+
}
|
|
132
|
+
function clampPositiveInteger(value, fallback, max) {
|
|
133
|
+
if (value === undefined)
|
|
134
|
+
return fallback;
|
|
135
|
+
if (!Number.isFinite(value) || value < 1)
|
|
136
|
+
return fallback;
|
|
137
|
+
return Math.min(Math.floor(value), max);
|
|
138
|
+
}
|
|
139
|
+
function clampDelay(value, fallback) {
|
|
140
|
+
if (value === undefined)
|
|
141
|
+
return fallback;
|
|
142
|
+
if (!Number.isFinite(value) || value < 0)
|
|
143
|
+
return fallback;
|
|
144
|
+
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
145
|
+
}
|
|
146
|
+
export function createDefaultProxyTransportRetryOptions(options = {}) {
|
|
147
|
+
return createRetryOptions(HttpRetryPreset.TransportTransient, options.extraErrorCodes ?? [], options.label ?? "Proxy transport");
|
|
148
|
+
}
|
|
149
|
+
export function normalizeProxyTransportRetryOptions(retry, options = {}) {
|
|
150
|
+
const label = options.label ?? "Proxy transport";
|
|
151
|
+
const extraErrorCodes = options.extraErrorCodes ?? [];
|
|
152
|
+
if (retry === undefined || retry === false) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
if (retry === true) {
|
|
156
|
+
return createRetryOptions(HttpRetryPreset.TransportTransient, extraErrorCodes, label);
|
|
157
|
+
}
|
|
158
|
+
if (typeof retry === "string") {
|
|
159
|
+
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
160
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${retry}`, label);
|
|
161
|
+
}
|
|
162
|
+
return createRetryOptions(retry, extraErrorCodes, label);
|
|
163
|
+
}
|
|
164
|
+
if (typeof retry !== "object" || retry === null) {
|
|
165
|
+
throw createInvalidRetryPolicyError(`${label} retry policy must be an object`, label);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(retry)) {
|
|
168
|
+
throw createInvalidRetryPolicyError(`${label} retry policy must be a plain object`, label);
|
|
169
|
+
}
|
|
170
|
+
validateRetryOptionsShape(retry, label);
|
|
171
|
+
const base = createRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient, extraErrorCodes, label);
|
|
172
|
+
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
173
|
+
return {
|
|
174
|
+
preset: retry.preset ?? base.preset,
|
|
175
|
+
attempts: clampPositiveInteger(retry.attempts, base.attempts, MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS),
|
|
176
|
+
methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
177
|
+
statusCodes: retry.statusCodes?.filter((status) => Number.isInteger(status)) ?? base.statusCodes,
|
|
178
|
+
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
179
|
+
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
180
|
+
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
181
|
+
maxDelayMs,
|
|
182
|
+
jitter: retry.jitter ?? base.jitter,
|
|
183
|
+
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
184
|
+
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function validateRetryOptionsShape(retry, label) {
|
|
188
|
+
if (retry.preset !== undefined && !hasOwnValue(HttpRetryPreset, retry.preset)) {
|
|
189
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${String(retry.preset)}`, label);
|
|
190
|
+
}
|
|
191
|
+
if (retry.delayStrategy !== undefined &&
|
|
192
|
+
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)) {
|
|
193
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry delay strategy: ${String(retry.delayStrategy)}`, label);
|
|
194
|
+
}
|
|
195
|
+
if (retry.jitter !== undefined && !hasOwnValue(HttpRetryJitter, retry.jitter)) {
|
|
196
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry jitter policy: ${String(retry.jitter)}`, label);
|
|
197
|
+
}
|
|
198
|
+
if (retry.retryAfter !== undefined && !hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
|
|
199
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry-after policy: ${String(retry.retryAfter)}`, label);
|
|
200
|
+
}
|
|
201
|
+
if (retry.unsafeMethodPolicy !== undefined &&
|
|
202
|
+
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)) {
|
|
203
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`, label);
|
|
204
|
+
}
|
|
205
|
+
if (retry.methods !== undefined) {
|
|
206
|
+
if (!Array.isArray(retry.methods)) {
|
|
207
|
+
throw createInvalidRetryPolicyError(`${label} retry methods must be an array`, label);
|
|
208
|
+
}
|
|
209
|
+
const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
|
|
210
|
+
if (nonStringMethods.length > 0) {
|
|
211
|
+
throw createInvalidRetryPolicyError(`${label} retry methods must contain only strings`, label);
|
|
212
|
+
}
|
|
213
|
+
const unknownMethods = retry.methods
|
|
214
|
+
.map((method) => method.toUpperCase())
|
|
215
|
+
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
216
|
+
if (unknownMethods.length > 0) {
|
|
217
|
+
throw createInvalidRetryPolicyError(`Unknown ${label} retry method(s): ${unknownMethods.join(", ")}`, label);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (retry.statusCodes !== undefined) {
|
|
221
|
+
if (!Array.isArray(retry.statusCodes)) {
|
|
222
|
+
throw createInvalidRetryPolicyError(`${label} retry statusCodes must be an array`, label);
|
|
223
|
+
}
|
|
224
|
+
const invalidStatusCodes = retry.statusCodes.filter((status) => !Number.isInteger(status) || Number(status) < 100 || Number(status) > 599);
|
|
225
|
+
if (invalidStatusCodes.length > 0) {
|
|
226
|
+
throw createInvalidRetryPolicyError(`${label} retry statusCodes must contain HTTP status integers in [100, 599]`, label);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (retry.errorCodes !== undefined) {
|
|
230
|
+
if (!Array.isArray(retry.errorCodes)) {
|
|
231
|
+
throw createInvalidRetryPolicyError(`${label} retry errorCodes must be an array`, label);
|
|
232
|
+
}
|
|
233
|
+
const nonStringErrorCodes = retry.errorCodes.filter((errorCode) => typeof errorCode !== "string");
|
|
234
|
+
if (nonStringErrorCodes.length > 0) {
|
|
235
|
+
throw createInvalidRetryPolicyError(`${label} retry errorCodes must contain only strings`, label);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (retry.preset === HttpRetryPreset.Off &&
|
|
239
|
+
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
240
|
+
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))) {
|
|
241
|
+
throw createInvalidRetryPolicyError(`${label} retry preset off cannot be combined with retry-enabling overrides`, label);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function validateUnsafeProxyTransportRetryMethods(options, label = "Proxy transport") {
|
|
245
|
+
if (options.unsafeMethodPolicy === HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const unsafeMethods = options.methods.filter((method) => UNSAFE_RETRY_METHODS.has(method.toUpperCase()));
|
|
249
|
+
if (unsafeMethods.length === 0)
|
|
250
|
+
return;
|
|
251
|
+
throw new ProviderError(`${label} retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
|
|
252
|
+
}
|
|
253
|
+
export function isProxyTransportRetryMethod(method, options) {
|
|
254
|
+
return options.methods
|
|
255
|
+
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
256
|
+
.includes(method.toUpperCase());
|
|
257
|
+
}
|
|
258
|
+
export function shouldRetryProxyTransportError(error, options) {
|
|
259
|
+
const code = retryErrorCode(error);
|
|
260
|
+
return Boolean(code && options.errorCodes.includes(code));
|
|
261
|
+
}
|
|
262
|
+
export function shouldRetryProxyTransportAttempt(input) {
|
|
263
|
+
const { error, explicitRetry, method, options, proxyUsed } = input;
|
|
264
|
+
if (!options || options.attempts <= 1)
|
|
265
|
+
return false;
|
|
266
|
+
if (!explicitRetry && !proxyUsed)
|
|
267
|
+
return false;
|
|
268
|
+
return (isProxyTransportRetryMethod(method, options) && shouldRetryProxyTransportError(error, options));
|
|
269
|
+
}
|
|
270
|
+
function retryAfterHeader(headers) {
|
|
271
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
272
|
+
if (name.toLowerCase() === "retry-after")
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
function parseRetryAfterMs(headers, now = Date.now()) {
|
|
278
|
+
const value = retryAfterHeader(headers);
|
|
279
|
+
if (!value)
|
|
280
|
+
return undefined;
|
|
281
|
+
const seconds = Number(value);
|
|
282
|
+
if (Number.isFinite(seconds)) {
|
|
283
|
+
return Math.max(0, Math.floor(seconds * 1_000));
|
|
284
|
+
}
|
|
285
|
+
const dateMs = Date.parse(value);
|
|
286
|
+
if (!Number.isNaN(dateMs)) {
|
|
287
|
+
return Math.max(0, dateMs - now);
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
export function computeProxyTransportRetryDelayMs(options, attemptIndex, headers) {
|
|
292
|
+
const multiplier = options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
293
|
+
? 2 ** Math.max(0, attemptIndex - 1)
|
|
294
|
+
: 1;
|
|
295
|
+
const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
|
|
296
|
+
const retryAfterMs = options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
297
|
+
? undefined
|
|
298
|
+
: headers
|
|
299
|
+
? parseRetryAfterMs(headers)
|
|
300
|
+
: undefined;
|
|
301
|
+
if (retryAfterMs !== undefined) {
|
|
302
|
+
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
303
|
+
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
304
|
+
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
305
|
+
}
|
|
306
|
+
return boundedRetryAfterMs;
|
|
307
|
+
}
|
|
308
|
+
switch (options.jitter) {
|
|
309
|
+
case HttpRetryJitter.None:
|
|
310
|
+
return configuredDelay;
|
|
311
|
+
case HttpRetryJitter.Equal:
|
|
312
|
+
return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
|
|
313
|
+
case HttpRetryJitter.Full:
|
|
314
|
+
return Math.floor(Math.random() * configuredDelay);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export function normalizeProxyAttemptIndex(value) {
|
|
318
|
+
if (value === undefined || !Number.isFinite(value))
|
|
319
|
+
return 0;
|
|
320
|
+
return Math.max(0, Math.floor(value));
|
|
321
|
+
}
|
|
322
|
+
export function computeProxyAttemptIndex(options) {
|
|
323
|
+
return (normalizeProxyAttemptIndex(options.baseProxyAttempt) +
|
|
324
|
+
normalizeProxyAttemptIndex(options.proxyAttemptOffset) +
|
|
325
|
+
normalizeProxyAttemptIndex(options.retryAttemptOffset));
|
|
326
|
+
}
|