@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.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 +218 -21
- package/CHANGELOG.md +54 -0
- package/README.md +147 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +120 -0
- package/bin/apifuse-pack-smoke.ts +423 -0
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -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 +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +834 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/ceremonies/index.ts +30 -3
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +214 -52
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +134 -2
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -44
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1282 -7
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +1726 -48
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +174 -15
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -5
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +945 -185
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1159 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1172 -76
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1118 -44
- package/src/composite.ts +0 -43
- package/src/runtime/tls.ts +0 -425
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/runtime/http.ts
CHANGED
|
@@ -1,256 +1,1016 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
|
|
3
1
|
import type { ProxyResolutionOptions } from "../config/loader";
|
|
4
|
-
import {
|
|
5
|
-
import { TransportError } from "../errors";
|
|
6
|
-
import {
|
|
2
|
+
import { resolveProxyConfigAsync } from "../config/loader";
|
|
3
|
+
import { ProviderError, TransportError } from "../errors";
|
|
4
|
+
import {
|
|
5
|
+
parseSseStream,
|
|
6
|
+
readableBytes,
|
|
7
|
+
readableLines,
|
|
8
|
+
readableTextChunks,
|
|
9
|
+
} from "../stream";
|
|
7
10
|
import type {
|
|
8
11
|
HttpClient,
|
|
12
|
+
HttpMethod,
|
|
9
13
|
HttpResponse,
|
|
14
|
+
HttpRetryOptions,
|
|
15
|
+
HttpRetrySummary,
|
|
16
|
+
HttpStreamResponse,
|
|
10
17
|
RequestOptions,
|
|
11
18
|
RequestWithMethodOptions,
|
|
12
|
-
|
|
19
|
+
SseMessage,
|
|
13
20
|
} from "../types";
|
|
21
|
+
import {
|
|
22
|
+
HttpRetryAfterPolicy,
|
|
23
|
+
HttpRetryDelayStrategy,
|
|
24
|
+
HttpRetryJitter,
|
|
25
|
+
HttpRetryPreset,
|
|
26
|
+
HttpRetryUnsafeMethodPolicy,
|
|
27
|
+
} from "../types";
|
|
28
|
+
import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
|
|
14
29
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const MISSING_PROXY_WARNING =
|
|
18
|
-
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
19
|
-
|
|
20
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
30
|
+
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
21
31
|
|
|
22
|
-
type
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
export type HttpClientOptions = ProxyResolutionOptions & {
|
|
33
|
+
warn?: (message: string) => void;
|
|
34
|
+
userAgent?: string;
|
|
35
|
+
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
25
36
|
};
|
|
26
37
|
|
|
27
|
-
type
|
|
28
|
-
|
|
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[];
|
|
29
54
|
};
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
type HttpStatusOutcome = {
|
|
57
|
+
kind: "http-status";
|
|
58
|
+
status: number;
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
retryable: boolean;
|
|
35
61
|
};
|
|
36
62
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
function isHttpStatusOutcome(
|
|
64
|
+
outcome: HttpResponse | HttpStatusOutcome,
|
|
65
|
+
): outcome is HttpStatusOutcome {
|
|
66
|
+
return "kind" in outcome && outcome.kind === "http-status";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"] as const;
|
|
70
|
+
const DEFAULT_RETRY_ERROR_CODES = [
|
|
71
|
+
"transport_network_error",
|
|
72
|
+
"transport_timeout",
|
|
73
|
+
] as const;
|
|
74
|
+
const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
|
|
75
|
+
const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503] as const;
|
|
76
|
+
const KNOWN_RETRY_METHODS = new Set([
|
|
77
|
+
"GET",
|
|
78
|
+
"HEAD",
|
|
79
|
+
"POST",
|
|
80
|
+
"PUT",
|
|
81
|
+
"DELETE",
|
|
82
|
+
"OPTIONS",
|
|
83
|
+
"TRACE",
|
|
84
|
+
"PATCH",
|
|
85
|
+
]);
|
|
86
|
+
const UNSAFE_RETRY_METHODS = new Set([
|
|
87
|
+
"POST",
|
|
88
|
+
"PUT",
|
|
89
|
+
"PATCH",
|
|
90
|
+
"DELETE",
|
|
91
|
+
"TRACE",
|
|
92
|
+
]);
|
|
93
|
+
const MAX_RETRY_ATTEMPTS = 8;
|
|
94
|
+
const MAX_RETRY_DELAY_MS = 30_000;
|
|
95
|
+
|
|
96
|
+
function hasOwnValue<T extends string>(
|
|
97
|
+
values: Record<string, T>,
|
|
98
|
+
value: unknown,
|
|
99
|
+
): value is T {
|
|
100
|
+
if (typeof value !== "string") return false;
|
|
101
|
+
return Object.values(values).some((candidate) => candidate === value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createInvalidRetryPolicyError(message: string): ProviderError {
|
|
105
|
+
return new ProviderError(message, { code: "retry_invalid_policy" });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createRetryOptions(preset: HttpRetryPreset): NormalizedRetryOptions {
|
|
109
|
+
switch (preset) {
|
|
110
|
+
case HttpRetryPreset.Off:
|
|
111
|
+
return {
|
|
112
|
+
preset,
|
|
113
|
+
attempts: 1,
|
|
114
|
+
methods: DEFAULT_RETRY_METHODS,
|
|
115
|
+
statusCodes: [],
|
|
116
|
+
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
117
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
118
|
+
baseDelayMs: 100,
|
|
119
|
+
maxDelayMs: 1_000,
|
|
120
|
+
jitter: HttpRetryJitter.Full,
|
|
121
|
+
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
122
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
123
|
+
};
|
|
124
|
+
case HttpRetryPreset.SafeRead:
|
|
125
|
+
return {
|
|
126
|
+
preset,
|
|
127
|
+
attempts: 3,
|
|
128
|
+
methods: DEFAULT_RETRY_METHODS,
|
|
129
|
+
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
130
|
+
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
131
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
132
|
+
baseDelayMs: 100,
|
|
133
|
+
maxDelayMs: 2_000,
|
|
134
|
+
jitter: HttpRetryJitter.Full,
|
|
135
|
+
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
136
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
137
|
+
};
|
|
138
|
+
case HttpRetryPreset.AggressiveRead:
|
|
139
|
+
return {
|
|
140
|
+
preset,
|
|
141
|
+
attempts: 4,
|
|
142
|
+
methods: DEFAULT_RETRY_METHODS,
|
|
143
|
+
statusCodes: SAFE_RETRY_STATUS_CODES,
|
|
144
|
+
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
145
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
146
|
+
baseDelayMs: 150,
|
|
147
|
+
maxDelayMs: 5_000,
|
|
148
|
+
jitter: HttpRetryJitter.Full,
|
|
149
|
+
retryAfter: HttpRetryAfterPolicy.Cap,
|
|
150
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
151
|
+
};
|
|
152
|
+
case HttpRetryPreset.RateLimitAware:
|
|
153
|
+
return {
|
|
154
|
+
preset,
|
|
155
|
+
attempts: 3,
|
|
156
|
+
methods: DEFAULT_RETRY_METHODS,
|
|
157
|
+
statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
|
|
158
|
+
errorCodes: ["transport_timeout"],
|
|
159
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
160
|
+
baseDelayMs: 250,
|
|
161
|
+
maxDelayMs: 5_000,
|
|
162
|
+
jitter: HttpRetryJitter.Equal,
|
|
163
|
+
retryAfter: HttpRetryAfterPolicy.Respect,
|
|
164
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
165
|
+
};
|
|
166
|
+
case HttpRetryPreset.TransportTransient:
|
|
167
|
+
return {
|
|
168
|
+
preset,
|
|
169
|
+
attempts: 3,
|
|
170
|
+
methods: DEFAULT_RETRY_METHODS,
|
|
171
|
+
statusCodes: [],
|
|
172
|
+
errorCodes: DEFAULT_RETRY_ERROR_CODES,
|
|
173
|
+
delayStrategy: HttpRetryDelayStrategy.Exponential,
|
|
174
|
+
baseDelayMs: 100,
|
|
175
|
+
maxDelayMs: 1_000,
|
|
176
|
+
jitter: HttpRetryJitter.Full,
|
|
177
|
+
retryAfter: HttpRetryAfterPolicy.Ignore,
|
|
178
|
+
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
179
|
+
};
|
|
40
180
|
}
|
|
181
|
+
throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function clampPositiveInteger(
|
|
185
|
+
value: number | undefined,
|
|
186
|
+
fallback: number,
|
|
187
|
+
max: number,
|
|
188
|
+
): number {
|
|
189
|
+
if (value === undefined) return fallback;
|
|
190
|
+
if (!Number.isFinite(value) || value < 1) return fallback;
|
|
191
|
+
return Math.min(Math.floor(value), max);
|
|
192
|
+
}
|
|
41
193
|
|
|
42
|
-
|
|
43
|
-
|
|
194
|
+
function clampDelay(value: number | undefined, fallback: number): number {
|
|
195
|
+
if (value === undefined) return fallback;
|
|
196
|
+
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
197
|
+
return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
|
|
44
198
|
}
|
|
45
199
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
200
|
+
function normalizeRetryOptions(
|
|
201
|
+
retry: RequestOptions["retry"],
|
|
202
|
+
): NormalizedRetryOptions | undefined {
|
|
203
|
+
if (retry === undefined || retry === false) {
|
|
204
|
+
return undefined;
|
|
49
205
|
}
|
|
206
|
+
if (retry === true) {
|
|
207
|
+
return createRetryOptions(HttpRetryPreset.TransportTransient);
|
|
208
|
+
}
|
|
209
|
+
if (typeof retry === "string") {
|
|
210
|
+
if (!hasOwnValue(HttpRetryPreset, retry)) {
|
|
211
|
+
throw createInvalidRetryPolicyError(
|
|
212
|
+
`Unknown HTTP retry preset: ${retry}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return createRetryOptions(retry);
|
|
216
|
+
}
|
|
217
|
+
if (typeof retry !== "object" || retry === null) {
|
|
218
|
+
throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(retry)) {
|
|
221
|
+
throw createInvalidRetryPolicyError(
|
|
222
|
+
"HTTP retry policy must be a plain object",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
validateRetryOptionsShape(retry);
|
|
227
|
+
const base = createRetryOptions(
|
|
228
|
+
retry.preset ?? HttpRetryPreset.TransportTransient,
|
|
229
|
+
);
|
|
230
|
+
const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
|
|
231
|
+
const normalized: NormalizedRetryOptions = {
|
|
232
|
+
preset: retry.preset ?? base.preset,
|
|
233
|
+
attempts: clampPositiveInteger(
|
|
234
|
+
retry.attempts,
|
|
235
|
+
base.attempts,
|
|
236
|
+
MAX_RETRY_ATTEMPTS,
|
|
237
|
+
),
|
|
238
|
+
methods:
|
|
239
|
+
retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
240
|
+
statusCodes:
|
|
241
|
+
retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
|
|
242
|
+
base.statusCodes,
|
|
243
|
+
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
244
|
+
delayStrategy: retry.delayStrategy ?? base.delayStrategy,
|
|
245
|
+
baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
|
|
246
|
+
maxDelayMs,
|
|
247
|
+
jitter: retry.jitter ?? base.jitter,
|
|
248
|
+
retryAfter: retry.retryAfter ?? base.retryAfter,
|
|
249
|
+
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
250
|
+
};
|
|
50
251
|
|
|
51
|
-
return
|
|
252
|
+
return normalized;
|
|
52
253
|
}
|
|
53
254
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
255
|
+
function validateRetryOptionsShape(retry: HttpRetryOptions): void {
|
|
256
|
+
if (
|
|
257
|
+
retry.preset !== undefined &&
|
|
258
|
+
!hasOwnValue(HttpRetryPreset, retry.preset)
|
|
259
|
+
) {
|
|
260
|
+
throw createInvalidRetryPolicyError(
|
|
261
|
+
`Unknown HTTP retry preset: ${String(retry.preset)}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
retry.delayStrategy !== undefined &&
|
|
266
|
+
!hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)
|
|
267
|
+
) {
|
|
268
|
+
throw createInvalidRetryPolicyError(
|
|
269
|
+
`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
retry.jitter !== undefined &&
|
|
274
|
+
!hasOwnValue(HttpRetryJitter, retry.jitter)
|
|
275
|
+
) {
|
|
276
|
+
throw createInvalidRetryPolicyError(
|
|
277
|
+
`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
retry.retryAfter !== undefined &&
|
|
282
|
+
!hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)
|
|
283
|
+
) {
|
|
284
|
+
throw createInvalidRetryPolicyError(
|
|
285
|
+
`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
retry.unsafeMethodPolicy !== undefined &&
|
|
290
|
+
!hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)
|
|
291
|
+
) {
|
|
292
|
+
throw createInvalidRetryPolicyError(
|
|
293
|
+
`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (retry.methods !== undefined) {
|
|
297
|
+
if (!Array.isArray(retry.methods)) {
|
|
298
|
+
throw createInvalidRetryPolicyError(
|
|
299
|
+
"HTTP retry methods must be an array",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const nonStringMethods = retry.methods.filter(
|
|
303
|
+
(method) => typeof method !== "string",
|
|
304
|
+
);
|
|
305
|
+
if (nonStringMethods.length > 0) {
|
|
306
|
+
throw createInvalidRetryPolicyError(
|
|
307
|
+
"HTTP retry methods must contain only strings",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const unknownMethods = retry.methods
|
|
311
|
+
.map((method) => method.toUpperCase())
|
|
312
|
+
.filter((method) => !KNOWN_RETRY_METHODS.has(method));
|
|
313
|
+
if (unknownMethods.length > 0) {
|
|
314
|
+
throw createInvalidRetryPolicyError(
|
|
315
|
+
`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (retry.statusCodes !== undefined) {
|
|
320
|
+
if (!Array.isArray(retry.statusCodes)) {
|
|
321
|
+
throw createInvalidRetryPolicyError(
|
|
322
|
+
"HTTP retry statusCodes must be an array",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const invalidStatusCodes = retry.statusCodes.filter(
|
|
326
|
+
(status) =>
|
|
327
|
+
!Number.isInteger(status) ||
|
|
328
|
+
Number(status) < 100 ||
|
|
329
|
+
Number(status) > 599,
|
|
330
|
+
);
|
|
331
|
+
if (invalidStatusCodes.length > 0) {
|
|
332
|
+
throw createInvalidRetryPolicyError(
|
|
333
|
+
"HTTP retry statusCodes must contain HTTP status integers in [100, 599]",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (retry.errorCodes !== undefined) {
|
|
338
|
+
if (!Array.isArray(retry.errorCodes)) {
|
|
339
|
+
throw createInvalidRetryPolicyError(
|
|
340
|
+
"HTTP retry errorCodes must be an array",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const nonStringErrorCodes = retry.errorCodes.filter(
|
|
344
|
+
(errorCode) => typeof errorCode !== "string",
|
|
345
|
+
);
|
|
346
|
+
if (nonStringErrorCodes.length > 0) {
|
|
347
|
+
throw createInvalidRetryPolicyError(
|
|
348
|
+
"HTTP retry errorCodes must contain only strings",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
retry.preset === HttpRetryPreset.Off &&
|
|
354
|
+
((retry.attempts !== undefined && retry.attempts > 1) ||
|
|
355
|
+
(retry.statusCodes !== undefined && retry.statusCodes.length > 0))
|
|
356
|
+
) {
|
|
357
|
+
throw createInvalidRetryPolicyError(
|
|
358
|
+
"HTTP retry preset off cannot be combined with retry-enabling overrides",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
63
362
|
|
|
64
|
-
|
|
65
|
-
|
|
363
|
+
function validateUnsafeRetryMethods(options: NormalizedRetryOptions): void {
|
|
364
|
+
if (
|
|
365
|
+
options.unsafeMethodPolicy ===
|
|
366
|
+
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
|
|
367
|
+
) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const unsafeMethods = options.methods.filter((method) =>
|
|
371
|
+
UNSAFE_RETRY_METHODS.has(method.toUpperCase()),
|
|
372
|
+
);
|
|
373
|
+
if (unsafeMethods.length === 0) return;
|
|
374
|
+
|
|
375
|
+
throw new ProviderError(
|
|
376
|
+
`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
|
|
377
|
+
{ code: "retry_unsafe_method" },
|
|
378
|
+
);
|
|
379
|
+
}
|
|
66
380
|
|
|
67
|
-
|
|
68
|
-
|
|
381
|
+
function isMethodRetryable(
|
|
382
|
+
method: HttpMethod,
|
|
383
|
+
options: NormalizedRetryOptions,
|
|
384
|
+
): boolean {
|
|
385
|
+
return options.methods
|
|
386
|
+
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
387
|
+
.includes(method.toUpperCase());
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function retryErrorCode(error: unknown): string | undefined {
|
|
391
|
+
if (error instanceof TransportError) {
|
|
392
|
+
return error.code;
|
|
393
|
+
}
|
|
394
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
395
|
+
const code = Reflect.get(error, "code");
|
|
396
|
+
return typeof code === "string" ? code : undefined;
|
|
69
397
|
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
70
400
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
headers: {
|
|
78
|
-
"Content-Type": "application/json",
|
|
79
|
-
...headers,
|
|
80
|
-
},
|
|
81
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
82
|
-
signal: controller.signal,
|
|
83
|
-
});
|
|
401
|
+
function retryErrorStatus(error: unknown): number | undefined {
|
|
402
|
+
if (error instanceof TransportError) {
|
|
403
|
+
return error.status ?? error.upstreamStatus;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
84
407
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
408
|
+
function shouldRetryTransportError(
|
|
409
|
+
error: unknown,
|
|
410
|
+
options: NormalizedRetryOptions,
|
|
411
|
+
): boolean {
|
|
412
|
+
const code = retryErrorCode(error);
|
|
413
|
+
return Boolean(code && options.errorCodes.includes(code));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function retryAfterHeader(headers: Record<string, string>): string | undefined {
|
|
417
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
418
|
+
if (name.toLowerCase() === "retry-after") return value;
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseRetryAfterMs(
|
|
424
|
+
headers: Record<string, string>,
|
|
425
|
+
now: number = Date.now(),
|
|
426
|
+
): number | undefined {
|
|
427
|
+
const value = retryAfterHeader(headers);
|
|
428
|
+
if (!value) return undefined;
|
|
429
|
+
const seconds = Number(value);
|
|
430
|
+
if (Number.isFinite(seconds)) {
|
|
431
|
+
return Math.max(0, Math.floor(seconds * 1_000));
|
|
432
|
+
}
|
|
433
|
+
const dateMs = Date.parse(value);
|
|
434
|
+
if (!Number.isNaN(dateMs)) {
|
|
435
|
+
return Math.max(0, dateMs - now);
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function computeRetryDelayMs(
|
|
441
|
+
options: NormalizedRetryOptions,
|
|
442
|
+
attemptIndex: number,
|
|
443
|
+
headers?: Record<string, string>,
|
|
444
|
+
): number {
|
|
445
|
+
const multiplier =
|
|
446
|
+
options.delayStrategy === HttpRetryDelayStrategy.Exponential
|
|
447
|
+
? 2 ** Math.max(0, attemptIndex - 1)
|
|
448
|
+
: 1;
|
|
449
|
+
const configuredDelay = Math.min(
|
|
450
|
+
options.baseDelayMs * multiplier,
|
|
451
|
+
options.maxDelayMs,
|
|
452
|
+
);
|
|
453
|
+
const retryAfterMs =
|
|
454
|
+
options.retryAfter === HttpRetryAfterPolicy.Ignore
|
|
455
|
+
? undefined
|
|
456
|
+
: headers
|
|
457
|
+
? parseRetryAfterMs(headers)
|
|
458
|
+
: undefined;
|
|
459
|
+
if (retryAfterMs !== undefined) {
|
|
460
|
+
const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
|
|
461
|
+
if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
|
|
462
|
+
return Math.min(boundedRetryAfterMs, configuredDelay);
|
|
463
|
+
}
|
|
464
|
+
return boundedRetryAfterMs;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
switch (options.jitter) {
|
|
468
|
+
case HttpRetryJitter.None:
|
|
469
|
+
return configuredDelay;
|
|
470
|
+
case HttpRetryJitter.Equal:
|
|
471
|
+
return Math.floor(
|
|
472
|
+
configuredDelay / 2 + Math.random() * (configuredDelay / 2),
|
|
94
473
|
);
|
|
474
|
+
case HttpRetryJitter.Full:
|
|
475
|
+
return Math.floor(Math.random() * configuredDelay);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function sleep(ms: number): Promise<void> {
|
|
480
|
+
if (ms <= 0) return;
|
|
481
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function toUpstreamHttpError(status: number): TransportError {
|
|
485
|
+
return new TransportError(`Upstream request failed with status ${status}`, {
|
|
486
|
+
code: "upstream_http_error",
|
|
487
|
+
status,
|
|
488
|
+
upstreamStatus: status,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
493
|
+
const needle = name.toLowerCase();
|
|
494
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === needle);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function withClientHeaders(
|
|
498
|
+
options: RequestOptions | undefined,
|
|
499
|
+
clientOptions: HttpClientOptions,
|
|
500
|
+
body: unknown,
|
|
501
|
+
): RequestOptions {
|
|
502
|
+
const headers: Record<string, string> = {
|
|
503
|
+
...(clientOptions.userAgent
|
|
504
|
+
? { "User-Agent": clientOptions.userAgent }
|
|
505
|
+
: {}),
|
|
506
|
+
...options?.headers,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
if (body !== undefined && !hasHeader(headers, "Content-Type")) {
|
|
510
|
+
headers["Content-Type"] = "application/json";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
...options,
|
|
515
|
+
headers,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
520
|
+
const contentType =
|
|
521
|
+
headers["content-type"] ??
|
|
522
|
+
headers["Content-Type"] ??
|
|
523
|
+
headers["CONTENT-TYPE"];
|
|
524
|
+
|
|
525
|
+
if (contentType?.includes("application/json")) {
|
|
526
|
+
return body ? JSON.parse(body) : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return body;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function parseJson<T = unknown>(body: string): T {
|
|
533
|
+
return JSON.parse(body);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function isTimeoutMessage(message: string): boolean {
|
|
537
|
+
return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function toHttpTransportError(error: unknown): TransportError {
|
|
541
|
+
if (error instanceof TransportError) {
|
|
542
|
+
if (error.code) {
|
|
543
|
+
return error;
|
|
95
544
|
}
|
|
96
545
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
546
|
+
if (isTimeoutMessage(error.message)) {
|
|
547
|
+
return new TransportError("Request timed out", {
|
|
548
|
+
code: "transport_timeout",
|
|
549
|
+
status: error.status ?? 0,
|
|
550
|
+
cause: error,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
100
553
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
554
|
+
if ((error.status ?? 0) === 0) {
|
|
555
|
+
return new TransportError(error.message || "Network error", {
|
|
556
|
+
code: "transport_network_error",
|
|
557
|
+
status: 0,
|
|
558
|
+
cause: error,
|
|
559
|
+
});
|
|
105
560
|
}
|
|
106
561
|
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
562
|
+
return error;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (error instanceof Error) {
|
|
566
|
+
const timeout =
|
|
567
|
+
error.name === "AbortError" ||
|
|
568
|
+
error.name === "TimeoutError" ||
|
|
569
|
+
isTimeoutMessage(error.message);
|
|
570
|
+
return new TransportError(timeout ? "Request timed out" : "Network error", {
|
|
571
|
+
code: timeout ? "transport_timeout" : "transport_network_error",
|
|
572
|
+
status: 0,
|
|
573
|
+
cause: error,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return new TransportError("Network error", {
|
|
578
|
+
code: "transport_network_error",
|
|
579
|
+
status: 0,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
584
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
585
|
+
const rawText = await response.text();
|
|
586
|
+
const data = parseHttpData(rawText, headers);
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
data,
|
|
590
|
+
headers,
|
|
591
|
+
json: async <T = unknown>() => {
|
|
592
|
+
const contentType =
|
|
593
|
+
headers["content-type"] ??
|
|
594
|
+
headers["Content-Type"] ??
|
|
595
|
+
headers["CONTENT-TYPE"];
|
|
596
|
+
return parseJson<T>(
|
|
597
|
+
contentType?.includes("application/json") && !rawText
|
|
598
|
+
? "null"
|
|
599
|
+
: rawText,
|
|
600
|
+
);
|
|
601
|
+
},
|
|
602
|
+
ok: response.status >= 200 && response.status < 300,
|
|
603
|
+
status: response.status,
|
|
604
|
+
text: async () => rawText,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function drainNativeResponseBody(response: Response): Promise<void> {
|
|
609
|
+
try {
|
|
610
|
+
await response.arrayBuffer();
|
|
611
|
+
} catch {
|
|
612
|
+
await response.body?.cancel().catch(() => undefined);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function requireNativeResponseBody(
|
|
617
|
+
response: Response,
|
|
618
|
+
): ReadableStream<Uint8Array> {
|
|
619
|
+
if (!response.body) {
|
|
620
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
621
|
+
code: "transport_stream_unavailable",
|
|
115
622
|
status: response.status,
|
|
116
|
-
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return response.body;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function toNativeHttpStreamResponse(response: Response): HttpStreamResponse {
|
|
629
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
630
|
+
const body = requireNativeResponseBody(response);
|
|
631
|
+
return {
|
|
632
|
+
body,
|
|
633
|
+
headers,
|
|
634
|
+
ok: response.status >= 200 && response.status < 300,
|
|
635
|
+
status: response.status,
|
|
636
|
+
bytes: () => readableBytes(body),
|
|
637
|
+
textChunks: () => readableTextChunks(body),
|
|
638
|
+
lines: () => readableLines(body),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function normalizeHttpMethod(method: string): HttpMethod {
|
|
643
|
+
switch (method.toUpperCase()) {
|
|
644
|
+
case "HEAD":
|
|
645
|
+
return "HEAD";
|
|
646
|
+
case "GET":
|
|
647
|
+
return "GET";
|
|
648
|
+
case "POST":
|
|
649
|
+
return "POST";
|
|
650
|
+
case "PUT":
|
|
651
|
+
return "PUT";
|
|
652
|
+
case "DELETE":
|
|
653
|
+
return "DELETE";
|
|
654
|
+
case "OPTIONS":
|
|
655
|
+
return "OPTIONS";
|
|
656
|
+
case "TRACE":
|
|
657
|
+
return "TRACE";
|
|
658
|
+
case "PATCH":
|
|
659
|
+
return "PATCH";
|
|
660
|
+
default:
|
|
661
|
+
throw new TransportError(`Unsupported HTTP method: ${method}`, {
|
|
662
|
+
code: "transport_invalid_method",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function isAbsoluteUrl(url: string): boolean {
|
|
668
|
+
return /^[a-z][a-z\d+\-.]*:/i.test(url);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function resolveHttpUrl(baseUrl: string | undefined, url: string): string {
|
|
672
|
+
return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
type NativeFetchInit = RequestInit & { proxy?: string };
|
|
676
|
+
|
|
677
|
+
async function resolveNativeProxy(
|
|
678
|
+
options: RequestOptions,
|
|
679
|
+
clientOptions: HttpClientOptions,
|
|
680
|
+
warn: (message: string) => void,
|
|
681
|
+
): Promise<string | undefined> {
|
|
682
|
+
const resolvedProxy = await resolveProxyConfigAsync({
|
|
683
|
+
proxy: options.proxy ?? clientOptions.proxy,
|
|
684
|
+
upstream: clientOptions.upstream,
|
|
685
|
+
apifuseConfig: clientOptions.apifuseConfig,
|
|
686
|
+
affinityKey: clientOptions.affinityKey,
|
|
687
|
+
telemetry: clientOptions.telemetry,
|
|
688
|
+
});
|
|
689
|
+
if (resolvedProxy.shouldWarn) {
|
|
690
|
+
warn(
|
|
691
|
+
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.",
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return resolvedProxy.url;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function assertNoHttpTransportOverrides(options: RequestOptions): void {
|
|
698
|
+
if ("profile" in options || "stealth" in options) {
|
|
699
|
+
throw new ProviderError(
|
|
700
|
+
"ctx.http does not accept stealth transport options. Use ctx.stealth.fetch() for browser-like impersonation.",
|
|
701
|
+
{ code: "http_transport_override_unsupported" },
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function normalizeNativeFetchBody(
|
|
707
|
+
body: unknown,
|
|
708
|
+
): string | ArrayBuffer | undefined {
|
|
709
|
+
const normalized = normalizeHttpRequestBody(body);
|
|
710
|
+
if (!Buffer.isBuffer(normalized)) {
|
|
711
|
+
return normalized;
|
|
712
|
+
}
|
|
713
|
+
const copied = new Uint8Array(normalized.byteLength);
|
|
714
|
+
copied.set(normalized);
|
|
715
|
+
return copied.buffer;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function fetchNativeHttp(
|
|
719
|
+
baseUrl: string | undefined,
|
|
720
|
+
url: string,
|
|
721
|
+
method: HttpMethod,
|
|
722
|
+
options: RequestOptions & { body?: unknown },
|
|
723
|
+
clientOptions: HttpClientOptions,
|
|
724
|
+
warn: (message: string) => void,
|
|
725
|
+
statusRetryCodes?: readonly number[],
|
|
726
|
+
): Promise<HttpResponse | HttpStatusOutcome> {
|
|
727
|
+
const requestUrl = appendQueryParams(
|
|
728
|
+
resolveHttpUrl(baseUrl, url),
|
|
729
|
+
options.params,
|
|
730
|
+
);
|
|
731
|
+
const controller = options.timeout ? new AbortController() : undefined;
|
|
732
|
+
const timeoutHandle = options.timeout
|
|
733
|
+
? setTimeout(() => controller?.abort(), options.timeout)
|
|
734
|
+
: undefined;
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
738
|
+
const requestInit: NativeFetchInit = {
|
|
739
|
+
headers: options.headers,
|
|
740
|
+
method,
|
|
741
|
+
...(proxy ? { proxy } : {}),
|
|
742
|
+
signal: controller?.signal,
|
|
117
743
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
throw error;
|
|
744
|
+
if (options.body !== undefined) {
|
|
745
|
+
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
121
746
|
}
|
|
747
|
+
const response = await fetch(requestUrl, {
|
|
748
|
+
...requestInit,
|
|
749
|
+
});
|
|
750
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
122
751
|
|
|
123
|
-
if (
|
|
752
|
+
if (statusRetryCodes && response.status >= 400) {
|
|
753
|
+
await drainNativeResponseBody(response);
|
|
754
|
+
return {
|
|
755
|
+
kind: "http-status",
|
|
756
|
+
status: response.status,
|
|
757
|
+
headers,
|
|
758
|
+
retryable: statusRetryCodes.includes(response.status),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
763
|
+
await drainNativeResponseBody(response);
|
|
124
764
|
throw new TransportError(
|
|
125
|
-
`
|
|
765
|
+
`Upstream request failed with status ${response.status}`,
|
|
126
766
|
{
|
|
127
|
-
code: "
|
|
128
|
-
|
|
767
|
+
code: "upstream_http_error",
|
|
768
|
+
status: response.status,
|
|
129
769
|
},
|
|
130
770
|
);
|
|
131
771
|
}
|
|
132
772
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} finally {
|
|
138
|
-
if (timeoutId) {
|
|
139
|
-
clearTimeout(timeoutId);
|
|
773
|
+
return toNativeHttpResponse(response);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (error instanceof SyntaxError) {
|
|
776
|
+
throw error;
|
|
140
777
|
}
|
|
778
|
+
throw toHttpTransportError(error);
|
|
779
|
+
} finally {
|
|
780
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
141
781
|
}
|
|
142
782
|
}
|
|
143
783
|
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
784
|
+
async function fetchNativeHttpStream(
|
|
785
|
+
baseUrl: string | undefined,
|
|
786
|
+
url: string,
|
|
787
|
+
method: HttpMethod,
|
|
788
|
+
options: RequestOptions & { body?: unknown },
|
|
789
|
+
clientOptions: HttpClientOptions,
|
|
790
|
+
warn: (message: string) => void,
|
|
791
|
+
): Promise<HttpStreamResponse> {
|
|
792
|
+
const requestUrl = appendQueryParams(
|
|
793
|
+
resolveHttpUrl(baseUrl, url),
|
|
794
|
+
options.params,
|
|
795
|
+
);
|
|
796
|
+
const controller = options.timeout ? new AbortController() : undefined;
|
|
797
|
+
const timeoutHandle = options.timeout
|
|
798
|
+
? setTimeout(() => controller?.abort(), options.timeout)
|
|
799
|
+
: undefined;
|
|
150
800
|
|
|
151
|
-
|
|
801
|
+
try {
|
|
802
|
+
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
803
|
+
const requestInit: NativeFetchInit = {
|
|
804
|
+
headers: options.headers,
|
|
805
|
+
method,
|
|
806
|
+
...(proxy ? { proxy } : {}),
|
|
807
|
+
signal: controller?.signal,
|
|
808
|
+
};
|
|
809
|
+
if (options.body !== undefined) {
|
|
810
|
+
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
811
|
+
}
|
|
812
|
+
const response = await fetch(requestUrl, {
|
|
813
|
+
...requestInit,
|
|
814
|
+
});
|
|
152
815
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
816
|
+
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
817
|
+
await drainNativeResponseBody(response);
|
|
818
|
+
throw new TransportError(
|
|
819
|
+
`Upstream request failed with status ${response.status}`,
|
|
820
|
+
{
|
|
821
|
+
code: "upstream_http_error",
|
|
822
|
+
status: response.status,
|
|
823
|
+
},
|
|
824
|
+
);
|
|
825
|
+
}
|
|
156
826
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
dispatcher: new undici.ProxyAgent(
|
|
162
|
-
proxy,
|
|
163
|
-
) as FetchProxyInit["dispatcher"],
|
|
164
|
-
};
|
|
827
|
+
return toNativeHttpStreamResponse(response);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error instanceof SyntaxError) {
|
|
830
|
+
throw error;
|
|
165
831
|
}
|
|
166
|
-
|
|
167
|
-
|
|
832
|
+
throw toHttpTransportError(error);
|
|
833
|
+
} finally {
|
|
834
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
168
835
|
}
|
|
169
|
-
|
|
170
|
-
return { proxy };
|
|
171
836
|
}
|
|
172
837
|
|
|
173
838
|
export function createHttpClient(
|
|
174
839
|
baseUrl?: string,
|
|
175
840
|
clientOptions: HttpClientOptions = {},
|
|
176
841
|
): HttpClient {
|
|
177
|
-
|
|
842
|
+
const warnedMessages = new Set<string>();
|
|
178
843
|
const warn = clientOptions.warn ?? console.warn;
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
function withClientHeaders(
|
|
183
|
-
options?: RequestOptions,
|
|
184
|
-
): RequestOptions | undefined {
|
|
185
|
-
const layer2Headers = stealthProfile
|
|
186
|
-
? generateLayer2Headers(stealthProfile)
|
|
187
|
-
: undefined;
|
|
188
|
-
|
|
189
|
-
if (!userAgent && !layer2Headers) {
|
|
190
|
-
return options;
|
|
844
|
+
const warnOnce = (message: string) => {
|
|
845
|
+
if (warnedMessages.has(message)) {
|
|
846
|
+
return;
|
|
191
847
|
}
|
|
848
|
+
warnedMessages.add(message);
|
|
849
|
+
warn(message);
|
|
850
|
+
};
|
|
192
851
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
852
|
+
async function request(
|
|
853
|
+
url: string,
|
|
854
|
+
method: string,
|
|
855
|
+
options: RequestOptions & { body?: unknown } = {},
|
|
856
|
+
): Promise<HttpResponse> {
|
|
857
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
858
|
+
throw new TransportError(
|
|
859
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
860
|
+
{ code: "transport_invalid_url" },
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
assertNoHttpTransportOverrides(options);
|
|
864
|
+
const headersOptions = withClientHeaders(
|
|
865
|
+
options,
|
|
866
|
+
clientOptions,
|
|
867
|
+
options.body,
|
|
868
|
+
);
|
|
869
|
+
const methodName = normalizeHttpMethod(method);
|
|
870
|
+
const retryOptions = normalizeRetryOptions(headersOptions.retry);
|
|
871
|
+
if (retryOptions) validateUnsafeRetryMethods(retryOptions);
|
|
872
|
+
const retryEnabled = Boolean(
|
|
873
|
+
retryOptions &&
|
|
874
|
+
retryOptions.attempts > 1 &&
|
|
875
|
+
isMethodRetryable(methodName, retryOptions),
|
|
876
|
+
);
|
|
877
|
+
const statusRetryEnabled = Boolean(
|
|
878
|
+
retryEnabled &&
|
|
879
|
+
retryOptions &&
|
|
880
|
+
retryOptions.statusCodes.length > 0 &&
|
|
881
|
+
headersOptions.throwOnHttpError !== false,
|
|
882
|
+
);
|
|
883
|
+
const attemptOptions: RequestOptions & { body?: unknown } =
|
|
884
|
+
statusRetryEnabled
|
|
885
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
886
|
+
: headersOptions;
|
|
887
|
+
|
|
888
|
+
const executeOnce = (): Promise<HttpResponse | HttpStatusOutcome> =>
|
|
889
|
+
fetchNativeHttp(
|
|
890
|
+
baseUrl,
|
|
891
|
+
url,
|
|
892
|
+
methodName,
|
|
893
|
+
attemptOptions,
|
|
894
|
+
clientOptions,
|
|
895
|
+
warnOnce,
|
|
896
|
+
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
897
|
+
);
|
|
202
898
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
899
|
+
if (!retryEnabled || !retryOptions) {
|
|
900
|
+
const outcome = await executeOnce();
|
|
901
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
902
|
+
throw toUpstreamHttpError(outcome.status);
|
|
903
|
+
}
|
|
904
|
+
return outcome;
|
|
905
|
+
}
|
|
211
906
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
907
|
+
let lastErrorCode: string | undefined;
|
|
908
|
+
let lastStatus: number | undefined;
|
|
909
|
+
for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
|
|
910
|
+
try {
|
|
911
|
+
const outcome = await executeOnce();
|
|
912
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
913
|
+
lastStatus = outcome.status;
|
|
914
|
+
if (outcome.retryable && attempt < retryOptions.attempts) {
|
|
915
|
+
await sleep(
|
|
916
|
+
computeRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
917
|
+
);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
throw toUpstreamHttpError(outcome.status);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const response = outcome;
|
|
924
|
+
if (
|
|
925
|
+
response.status >= 400 &&
|
|
926
|
+
headersOptions.throwOnHttpError !== false
|
|
927
|
+
) {
|
|
928
|
+
throw toUpstreamHttpError(response.status);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (attempt > 1) {
|
|
932
|
+
const summary: HttpRetrySummary = {
|
|
933
|
+
attempts: attempt,
|
|
934
|
+
retries: attempt - 1,
|
|
935
|
+
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
936
|
+
transport: "native",
|
|
937
|
+
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
938
|
+
...(lastStatus ? { lastStatus } : {}),
|
|
939
|
+
};
|
|
940
|
+
clientOptions.onRetrySummary?.(summary);
|
|
941
|
+
}
|
|
942
|
+
return response;
|
|
943
|
+
} catch (error) {
|
|
944
|
+
lastErrorCode = retryErrorCode(error);
|
|
945
|
+
lastStatus = retryErrorStatus(error);
|
|
946
|
+
if (
|
|
947
|
+
attempt < retryOptions.attempts &&
|
|
948
|
+
shouldRetryTransportError(error, retryOptions)
|
|
949
|
+
) {
|
|
950
|
+
await sleep(computeRetryDelayMs(retryOptions, attempt));
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
215
955
|
}
|
|
216
956
|
|
|
217
|
-
|
|
957
|
+
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
958
|
+
code: "retry_exhausted",
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function streamRequest(
|
|
963
|
+
url: string,
|
|
964
|
+
method: string,
|
|
965
|
+
options: RequestOptions & { body?: unknown } = {},
|
|
966
|
+
): Promise<HttpStreamResponse> {
|
|
967
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
968
|
+
throw new TransportError(
|
|
969
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
970
|
+
{ code: "transport_invalid_url" },
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
assertNoHttpTransportOverrides(options);
|
|
974
|
+
const headersOptions = withClientHeaders(
|
|
975
|
+
options,
|
|
976
|
+
clientOptions,
|
|
977
|
+
options.body,
|
|
978
|
+
);
|
|
979
|
+
const methodName = normalizeHttpMethod(method);
|
|
980
|
+
return fetchNativeHttpStream(
|
|
981
|
+
baseUrl,
|
|
982
|
+
url,
|
|
983
|
+
methodName,
|
|
984
|
+
headersOptions,
|
|
985
|
+
clientOptions,
|
|
986
|
+
warnOnce,
|
|
987
|
+
);
|
|
218
988
|
}
|
|
219
989
|
|
|
220
990
|
return {
|
|
221
|
-
request: (url, options: RequestWithMethodOptions = {}) =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
body,
|
|
246
|
-
}),
|
|
247
|
-
delete: (url, options) =>
|
|
248
|
-
doRequest(
|
|
249
|
-
baseUrl,
|
|
250
|
-
url,
|
|
251
|
-
"DELETE",
|
|
252
|
-
resolveRequestProxy(options),
|
|
253
|
-
withClientHeaders(options),
|
|
254
|
-
),
|
|
991
|
+
request: async (url, options: RequestWithMethodOptions = {}) =>
|
|
992
|
+
request(url, options.method ?? "GET", options),
|
|
993
|
+
get: async (url, options) => request(url, "GET", options),
|
|
994
|
+
post: async (url, body, options) =>
|
|
995
|
+
request(url, "POST", { ...options, body }),
|
|
996
|
+
put: async (url, body, options) =>
|
|
997
|
+
request(url, "PUT", { ...options, body }),
|
|
998
|
+
delete: async (url, options) => request(url, "DELETE", options),
|
|
999
|
+
stream: async (url, options: RequestWithMethodOptions = {}) =>
|
|
1000
|
+
streamRequest(url, options.method ?? "GET", options),
|
|
1001
|
+
sse: async (
|
|
1002
|
+
url,
|
|
1003
|
+
options: RequestWithMethodOptions = {},
|
|
1004
|
+
): Promise<AsyncIterable<SseMessage>> => {
|
|
1005
|
+
const headers = {
|
|
1006
|
+
Accept: "text/event-stream",
|
|
1007
|
+
...options.headers,
|
|
1008
|
+
};
|
|
1009
|
+
const response = await streamRequest(url, options.method ?? "GET", {
|
|
1010
|
+
...options,
|
|
1011
|
+
headers,
|
|
1012
|
+
});
|
|
1013
|
+
return parseSseStream(response.body);
|
|
1014
|
+
},
|
|
255
1015
|
};
|
|
256
1016
|
}
|