@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/AUTHORING.md +134 -0
  2. package/CHANGELOG.md +61 -0
  3. package/README.md +21 -9
  4. package/SUBMISSION.md +10 -11
  5. package/bin/apifuse-pack-check.ts +22 -0
  6. package/bin/apifuse-submit-check.ts +1747 -388
  7. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  8. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  9. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  10. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  13. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  14. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  15. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  16. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  17. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  18. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  19. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  20. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  21. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  22. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  23. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  24. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  25. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  26. package/dist/auth-turn/index.d.ts +195 -0
  27. package/dist/auth-turn/index.js +133 -0
  28. package/dist/auth.d.ts +76 -0
  29. package/dist/auth.js +436 -0
  30. package/dist/ceremonies/index.js +7 -31
  31. package/dist/cli/create.js +45 -30
  32. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  33. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  34. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  35. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  36. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  37. package/dist/cli/templates/provider/README.md.tpl +163 -0
  38. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  39. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  40. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  41. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  42. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  43. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  44. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  46. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  48. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  49. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  50. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  51. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  52. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  53. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  54. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  55. package/dist/contract.js +1 -0
  56. package/dist/define.d.ts +6 -1
  57. package/dist/define.js +140 -70
  58. package/dist/index.d.ts +3 -2
  59. package/dist/index.js +2 -1
  60. package/dist/lint.d.ts +1 -0
  61. package/dist/lint.js +27 -0
  62. package/dist/provider.d.ts +4 -2
  63. package/dist/provider.js +2 -1
  64. package/dist/runtime/auth-flow.js +2 -0
  65. package/dist/runtime/browser.js +203 -0
  66. package/dist/runtime/http.js +46 -336
  67. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  68. package/dist/runtime/proxy-retry-policy.js +326 -0
  69. package/dist/runtime/stealth.d.ts +5 -2
  70. package/dist/runtime/stealth.js +206 -206
  71. package/dist/server/index.d.ts +4 -0
  72. package/dist/server/index.js +4 -0
  73. package/dist/server/self-test-input-tokens.d.ts +1 -0
  74. package/dist/server/self-test-input-tokens.js +37 -0
  75. package/dist/server/self-test-redaction.d.ts +20 -0
  76. package/dist/server/self-test-redaction.js +70 -0
  77. package/dist/server/self-test-token.d.ts +30 -0
  78. package/dist/server/self-test-token.js +50 -0
  79. package/dist/server/self-test.d.ts +98 -0
  80. package/dist/server/self-test.js +555 -0
  81. package/dist/server/serve.d.ts +6 -0
  82. package/dist/server/serve.js +38 -10
  83. package/dist/server/types.d.ts +1 -0
  84. package/dist/server/types.js +1 -0
  85. package/dist/testing/run.js +5 -1
  86. package/dist/types.d.ts +150 -0
  87. package/package.json +9 -2
  88. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  89. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  90. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  91. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  92. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  93. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  94. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  95. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  96. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  97. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  98. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  99. package/src/auth-turn/fixtures/valid/form.json +14 -0
  100. package/src/auth-turn/fixtures/valid/message.json +13 -0
  101. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  102. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  103. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  104. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  105. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  106. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  107. package/src/auth-turn/index.ts +177 -0
  108. package/src/auth.ts +786 -0
  109. package/src/ceremonies/index.ts +9 -43
  110. package/src/cli/create.ts +60 -97
  111. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  112. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  113. package/src/cli/templates/provider/README.md.tpl +7 -4
  114. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  115. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  116. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  117. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  118. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  119. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  120. package/src/contract.ts +1 -0
  121. package/src/define.ts +198 -71
  122. package/src/index.ts +16 -0
  123. package/src/lint.ts +33 -0
  124. package/src/provider.ts +27 -0
  125. package/src/runtime/auth-flow.ts +2 -0
  126. package/src/runtime/browser.ts +293 -1
  127. package/src/runtime/http.ts +84 -530
  128. package/src/runtime/proxy-retry-policy.ts +469 -0
  129. package/src/runtime/stealth.ts +269 -353
  130. package/src/server/index.ts +36 -0
  131. package/src/server/self-test-input-tokens.ts +46 -0
  132. package/src/server/self-test-redaction.ts +97 -0
  133. package/src/server/self-test-token.ts +70 -0
  134. package/src/server/self-test.ts +725 -0
  135. package/src/server/serve.ts +75 -6
  136. package/src/server/types.ts +1 -0
  137. package/src/testing/run.ts +9 -1
  138. package/src/types.ts +186 -0
@@ -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
- HttpRetryAfterPolicy,
23
- HttpRetryDelayStrategy,
24
- HttpRetryJitter,
25
- HttpRetryPreset,
26
- HttpRetryUnsafeMethodPolicy,
27
- } from "../types";
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,447 +33,24 @@ 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;
59
39
  headers: Record<string, string>;
60
40
  retryable: boolean;
41
+ proxyUsed: boolean;
61
42
  };
62
43
 
44
+ type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome;
45
+
46
+ type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
47
+
63
48
  function isHttpStatusOutcome(
64
49
  outcome: HttpResponse | HttpStatusOutcome,
65
50
  ): outcome is HttpStatusOutcome {
66
51
  return "kind" in outcome && outcome.kind === "http-status";
67
52
  }
68
53
 
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
- };
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
- }
193
-
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);
198
- }
199
-
200
- function normalizeRetryOptions(
201
- retry: RequestOptions["retry"],
202
- ): NormalizedRetryOptions | undefined {
203
- if (retry === undefined || retry === false) {
204
- return undefined;
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
- };
251
-
252
- return normalized;
253
- }
254
-
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
- }
362
-
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
- }
380
-
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;
397
- }
398
- return undefined;
399
- }
400
-
401
- function retryErrorStatus(error: unknown): number | undefined {
402
- if (error instanceof TransportError) {
403
- return error.status ?? error.upstreamStatus;
404
- }
405
- return undefined;
406
- }
407
-
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),
473
- );
474
- case HttpRetryJitter.Full:
475
- return Math.floor(Math.random() * configuredDelay);
476
- }
477
- }
478
-
479
54
  async function sleep(ms: number): Promise<void> {
480
55
  if (ms <= 0) return;
481
56
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -500,9 +75,7 @@ function withClientHeaders(
500
75
  body: unknown,
501
76
  ): RequestOptions {
502
77
  const headers: Record<string, string> = {
503
- ...(clientOptions.userAgent
504
- ? { "User-Agent": clientOptions.userAgent }
505
- : {}),
78
+ ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
506
79
  ...options?.headers,
507
80
  };
508
81
 
@@ -517,10 +90,7 @@ function withClientHeaders(
517
90
  }
518
91
 
519
92
  function parseHttpData(body: string, headers: Record<string, string>): unknown {
520
- const contentType =
521
- headers["content-type"] ??
522
- headers["Content-Type"] ??
523
- headers["CONTENT-TYPE"];
93
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
524
94
 
525
95
  if (contentType?.includes("application/json")) {
526
96
  return body ? JSON.parse(body) : null;
@@ -582,7 +152,8 @@ function toHttpTransportError(error: unknown): TransportError {
582
152
 
583
153
  async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
584
154
  const headers = Object.fromEntries(response.headers.entries());
585
- const rawText = await response.text();
155
+ const bodyBytes = new Uint8Array(await response.arrayBuffer());
156
+ const rawText = new TextDecoder().decode(bodyBytes);
586
157
  const data = parseHttpData(rawText, headers);
587
158
 
588
159
  return {
@@ -590,18 +161,15 @@ async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
590
161
  headers,
591
162
  json: async <T = unknown>() => {
592
163
  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
- );
164
+ headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
165
+ return parseJson<T>(contentType?.includes("application/json") && !rawText ? "null" : rawText);
601
166
  },
602
167
  ok: response.status >= 200 && response.status < 300,
603
168
  status: response.status,
604
169
  text: async () => rawText,
170
+ arrayBuffer: async () =>
171
+ bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
172
+ bytes: async () => bodyBytes.slice(0),
605
173
  };
606
174
  }
607
175
 
@@ -613,9 +181,7 @@ async function drainNativeResponseBody(response: Response): Promise<void> {
613
181
  }
614
182
  }
615
183
 
616
- function requireNativeResponseBody(
617
- response: Response,
618
- ): ReadableStream<Uint8Array> {
184
+ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Array> {
619
185
  if (!response.body) {
620
186
  throw new TransportError("Response body stream is unavailable", {
621
187
  code: "transport_stream_unavailable",
@@ -678,12 +244,18 @@ async function resolveNativeProxy(
678
244
  options: RequestOptions,
679
245
  clientOptions: HttpClientOptions,
680
246
  warn: (message: string) => void,
247
+ proxyAttemptOffset = 0,
681
248
  ): Promise<string | undefined> {
682
249
  const resolvedProxy = await resolveProxyConfigAsync({
683
250
  proxy: options.proxy ?? clientOptions.proxy,
684
251
  upstream: clientOptions.upstream,
685
252
  apifuseConfig: clientOptions.apifuseConfig,
253
+ proxyPolicy: clientOptions.proxyPolicy,
686
254
  affinityKey: clientOptions.affinityKey,
255
+ proxyAttempt: computeProxyAttemptIndex({
256
+ baseProxyAttempt: clientOptions.proxyAttempt,
257
+ retryAttemptOffset: proxyAttemptOffset,
258
+ }),
687
259
  telemetry: clientOptions.telemetry,
688
260
  });
689
261
  if (resolvedProxy.shouldWarn) {
@@ -703,9 +275,7 @@ function assertNoHttpTransportOverrides(options: RequestOptions): void {
703
275
  }
704
276
  }
705
277
 
706
- function normalizeNativeFetchBody(
707
- body: unknown,
708
- ): string | ArrayBuffer | undefined {
278
+ function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefined {
709
279
  const normalized = normalizeHttpRequestBody(body);
710
280
  if (!Buffer.isBuffer(normalized)) {
711
281
  return normalized;
@@ -723,18 +293,17 @@ async function fetchNativeHttp(
723
293
  clientOptions: HttpClientOptions,
724
294
  warn: (message: string) => void,
725
295
  statusRetryCodes?: readonly number[],
726
- ): Promise<HttpResponse | HttpStatusOutcome> {
727
- const requestUrl = appendQueryParams(
728
- resolveHttpUrl(baseUrl, url),
729
- options.params,
730
- );
296
+ proxyAttemptOffset = 0,
297
+ ): Promise<NativeHttpAttemptOutcome> {
298
+ const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
731
299
  const controller = options.timeout ? new AbortController() : undefined;
732
300
  const timeoutHandle = options.timeout
733
301
  ? setTimeout(() => controller?.abort(), options.timeout)
734
302
  : undefined;
735
303
 
304
+ let proxy: string | undefined;
736
305
  try {
737
- const proxy = await resolveNativeProxy(options, clientOptions, warn);
306
+ proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
738
307
  const requestInit: NativeFetchInit = {
739
308
  headers: options.headers,
740
309
  method,
@@ -756,18 +325,16 @@ async function fetchNativeHttp(
756
325
  status: response.status,
757
326
  headers,
758
327
  retryable: statusRetryCodes.includes(response.status),
328
+ proxyUsed: Boolean(proxy),
759
329
  };
760
330
  }
761
331
 
762
332
  if (response.status >= 400 && options.throwOnHttpError !== false) {
763
333
  await drainNativeResponseBody(response);
764
- throw new TransportError(
765
- `Upstream request failed with status ${response.status}`,
766
- {
767
- code: "upstream_http_error",
768
- status: response.status,
769
- },
770
- );
334
+ throw new TransportError(`Upstream request failed with status ${response.status}`, {
335
+ code: "upstream_http_error",
336
+ status: response.status,
337
+ });
771
338
  }
772
339
 
773
340
  return toNativeHttpResponse(response);
@@ -775,7 +342,9 @@ async function fetchNativeHttp(
775
342
  if (error instanceof SyntaxError) {
776
343
  throw error;
777
344
  }
778
- throw toHttpTransportError(error);
345
+ const transportError = toHttpTransportError(error) as NativeHttpAttemptError;
346
+ transportError.proxyUsed = Boolean(proxy);
347
+ throw transportError;
779
348
  } finally {
780
349
  if (timeoutHandle) clearTimeout(timeoutHandle);
781
350
  }
@@ -789,10 +358,7 @@ async function fetchNativeHttpStream(
789
358
  clientOptions: HttpClientOptions,
790
359
  warn: (message: string) => void,
791
360
  ): Promise<HttpStreamResponse> {
792
- const requestUrl = appendQueryParams(
793
- resolveHttpUrl(baseUrl, url),
794
- options.params,
795
- );
361
+ const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
796
362
  const controller = options.timeout ? new AbortController() : undefined;
797
363
  const timeoutHandle = options.timeout
798
364
  ? setTimeout(() => controller?.abort(), options.timeout)
@@ -815,13 +381,10 @@ async function fetchNativeHttpStream(
815
381
 
816
382
  if (response.status >= 400 && options.throwOnHttpError !== false) {
817
383
  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
- );
384
+ throw new TransportError(`Upstream request failed with status ${response.status}`, {
385
+ code: "upstream_http_error",
386
+ status: response.status,
387
+ });
825
388
  }
826
389
 
827
390
  return toNativeHttpStreamResponse(response);
@@ -861,31 +424,32 @@ export function createHttpClient(
861
424
  );
862
425
  }
863
426
  assertNoHttpTransportOverrides(options);
864
- const headersOptions = withClientHeaders(
865
- options,
866
- clientOptions,
867
- options.body,
868
- );
427
+ const headersOptions = withClientHeaders(options, clientOptions, options.body);
869
428
  const methodName = normalizeHttpMethod(method);
870
- const retryOptions = normalizeRetryOptions(headersOptions.retry);
871
- if (retryOptions) validateUnsafeRetryMethods(retryOptions);
429
+ const explicitRetry = headersOptions.retry !== undefined;
430
+ const retryOptions =
431
+ normalizeProxyTransportRetryOptions(headersOptions.retry, {
432
+ label: "HTTP",
433
+ }) ??
434
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
435
+ if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
872
436
  const retryEnabled = Boolean(
873
437
  retryOptions &&
874
438
  retryOptions.attempts > 1 &&
875
- isMethodRetryable(methodName, retryOptions),
439
+ isProxyTransportRetryMethod(methodName, retryOptions),
876
440
  );
877
441
  const statusRetryEnabled = Boolean(
878
442
  retryEnabled &&
443
+ explicitRetry &&
879
444
  retryOptions &&
880
445
  retryOptions.statusCodes.length > 0 &&
881
446
  headersOptions.throwOnHttpError !== false,
882
447
  );
883
- const attemptOptions: RequestOptions & { body?: unknown } =
884
- statusRetryEnabled
885
- ? { ...headersOptions, throwOnHttpError: false }
886
- : headersOptions;
448
+ const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
449
+ ? { ...headersOptions, throwOnHttpError: false }
450
+ : headersOptions;
887
451
 
888
- const executeOnce = (): Promise<HttpResponse | HttpStatusOutcome> =>
452
+ const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
889
453
  fetchNativeHttp(
890
454
  baseUrl,
891
455
  url,
@@ -894,6 +458,7 @@ export function createHttpClient(
894
458
  clientOptions,
895
459
  warnOnce,
896
460
  statusRetryEnabled ? retryOptions?.statusCodes : undefined,
461
+ proxyAttemptOffset,
897
462
  );
898
463
 
899
464
  if (!retryEnabled || !retryOptions) {
@@ -908,23 +473,18 @@ export function createHttpClient(
908
473
  let lastStatus: number | undefined;
909
474
  for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
910
475
  try {
911
- const outcome = await executeOnce();
476
+ const outcome = await executeOnce(attempt - 1);
912
477
  if (isHttpStatusOutcome(outcome)) {
913
478
  lastStatus = outcome.status;
914
479
  if (outcome.retryable && attempt < retryOptions.attempts) {
915
- await sleep(
916
- computeRetryDelayMs(retryOptions, attempt, outcome.headers),
917
- );
480
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
918
481
  continue;
919
482
  }
920
483
  throw toUpstreamHttpError(outcome.status);
921
484
  }
922
485
 
923
486
  const response = outcome;
924
- if (
925
- response.status >= 400 &&
926
- headersOptions.throwOnHttpError !== false
927
- ) {
487
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
928
488
  throw toUpstreamHttpError(response.status);
929
489
  }
930
490
 
@@ -941,13 +501,20 @@ export function createHttpClient(
941
501
  }
942
502
  return response;
943
503
  } catch (error) {
944
- lastErrorCode = retryErrorCode(error);
945
- lastStatus = retryErrorStatus(error);
504
+ lastErrorCode = proxyTransportRetryErrorCode(error);
505
+ lastStatus = proxyTransportRetryErrorStatus(error);
506
+ const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
946
507
  if (
947
508
  attempt < retryOptions.attempts &&
948
- shouldRetryTransportError(error, retryOptions)
509
+ shouldRetryProxyTransportAttempt({
510
+ error,
511
+ explicitRetry,
512
+ method: methodName,
513
+ options: retryOptions,
514
+ proxyUsed,
515
+ })
949
516
  ) {
950
- await sleep(computeRetryDelayMs(retryOptions, attempt));
517
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
951
518
  continue;
952
519
  }
953
520
  throw error;
@@ -971,30 +538,17 @@ export function createHttpClient(
971
538
  );
972
539
  }
973
540
  assertNoHttpTransportOverrides(options);
974
- const headersOptions = withClientHeaders(
975
- options,
976
- clientOptions,
977
- options.body,
978
- );
541
+ const headersOptions = withClientHeaders(options, clientOptions, options.body);
979
542
  const methodName = normalizeHttpMethod(method);
980
- return fetchNativeHttpStream(
981
- baseUrl,
982
- url,
983
- methodName,
984
- headersOptions,
985
- clientOptions,
986
- warnOnce,
987
- );
543
+ return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
988
544
  }
989
545
 
990
546
  return {
991
547
  request: async (url, options: RequestWithMethodOptions = {}) =>
992
548
  request(url, options.method ?? "GET", options),
993
549
  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 }),
550
+ post: async (url, body, options) => request(url, "POST", { ...options, body }),
551
+ put: async (url, body, options) => request(url, "PUT", { ...options, body }),
998
552
  delete: async (url, options) => request(url, "DELETE", options),
999
553
  stream: async (url, options: RequestWithMethodOptions = {}) =>
1000
554
  streamRequest(url, options.method ?? "GET", options),