@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,317 +1,12 @@
1
1
  import { resolveProxyConfigAsync } from "../config/loader";
2
2
  import { ProviderError, TransportError } from "../errors";
3
- import { parseSseStream, readableBytes, readableLines, readableTextChunks, } from "../stream";
4
- import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types";
3
+ import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream";
4
+ import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy";
5
5
  import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
6
6
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
7
7
  function isHttpStatusOutcome(outcome) {
8
8
  return "kind" in outcome && outcome.kind === "http-status";
9
9
  }
10
- const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
11
- const DEFAULT_RETRY_ERROR_CODES = [
12
- "transport_network_error",
13
- "transport_timeout",
14
- ];
15
- const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
16
- const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503];
17
- const KNOWN_RETRY_METHODS = new Set([
18
- "GET",
19
- "HEAD",
20
- "POST",
21
- "PUT",
22
- "DELETE",
23
- "OPTIONS",
24
- "TRACE",
25
- "PATCH",
26
- ]);
27
- const UNSAFE_RETRY_METHODS = new Set([
28
- "POST",
29
- "PUT",
30
- "PATCH",
31
- "DELETE",
32
- "TRACE",
33
- ]);
34
- const MAX_RETRY_ATTEMPTS = 8;
35
- const MAX_RETRY_DELAY_MS = 30_000;
36
- function hasOwnValue(values, value) {
37
- if (typeof value !== "string")
38
- return false;
39
- return Object.values(values).some((candidate) => candidate === value);
40
- }
41
- function createInvalidRetryPolicyError(message) {
42
- return new ProviderError(message, { code: "retry_invalid_policy" });
43
- }
44
- function createRetryOptions(preset) {
45
- switch (preset) {
46
- case HttpRetryPreset.Off:
47
- return {
48
- preset,
49
- attempts: 1,
50
- methods: DEFAULT_RETRY_METHODS,
51
- statusCodes: [],
52
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
53
- delayStrategy: HttpRetryDelayStrategy.Exponential,
54
- baseDelayMs: 100,
55
- maxDelayMs: 1_000,
56
- jitter: HttpRetryJitter.Full,
57
- retryAfter: HttpRetryAfterPolicy.Ignore,
58
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
59
- };
60
- case HttpRetryPreset.SafeRead:
61
- return {
62
- preset,
63
- attempts: 3,
64
- methods: DEFAULT_RETRY_METHODS,
65
- statusCodes: SAFE_RETRY_STATUS_CODES,
66
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
67
- delayStrategy: HttpRetryDelayStrategy.Exponential,
68
- baseDelayMs: 100,
69
- maxDelayMs: 2_000,
70
- jitter: HttpRetryJitter.Full,
71
- retryAfter: HttpRetryAfterPolicy.Cap,
72
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
73
- };
74
- case HttpRetryPreset.AggressiveRead:
75
- return {
76
- preset,
77
- attempts: 4,
78
- methods: DEFAULT_RETRY_METHODS,
79
- statusCodes: SAFE_RETRY_STATUS_CODES,
80
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
81
- delayStrategy: HttpRetryDelayStrategy.Exponential,
82
- baseDelayMs: 150,
83
- maxDelayMs: 5_000,
84
- jitter: HttpRetryJitter.Full,
85
- retryAfter: HttpRetryAfterPolicy.Cap,
86
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
87
- };
88
- case HttpRetryPreset.RateLimitAware:
89
- return {
90
- preset,
91
- attempts: 3,
92
- methods: DEFAULT_RETRY_METHODS,
93
- statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
94
- errorCodes: ["transport_timeout"],
95
- delayStrategy: HttpRetryDelayStrategy.Exponential,
96
- baseDelayMs: 250,
97
- maxDelayMs: 5_000,
98
- jitter: HttpRetryJitter.Equal,
99
- retryAfter: HttpRetryAfterPolicy.Respect,
100
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
101
- };
102
- case HttpRetryPreset.TransportTransient:
103
- return {
104
- preset,
105
- attempts: 3,
106
- methods: DEFAULT_RETRY_METHODS,
107
- statusCodes: [],
108
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
109
- delayStrategy: HttpRetryDelayStrategy.Exponential,
110
- baseDelayMs: 100,
111
- maxDelayMs: 1_000,
112
- jitter: HttpRetryJitter.Full,
113
- retryAfter: HttpRetryAfterPolicy.Ignore,
114
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
115
- };
116
- }
117
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
118
- }
119
- function clampPositiveInteger(value, fallback, max) {
120
- if (value === undefined)
121
- return fallback;
122
- if (!Number.isFinite(value) || value < 1)
123
- return fallback;
124
- return Math.min(Math.floor(value), max);
125
- }
126
- function clampDelay(value, fallback) {
127
- if (value === undefined)
128
- return fallback;
129
- if (!Number.isFinite(value) || value < 0)
130
- return fallback;
131
- return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
132
- }
133
- function normalizeRetryOptions(retry) {
134
- if (retry === undefined || retry === false) {
135
- return undefined;
136
- }
137
- if (retry === true) {
138
- return createRetryOptions(HttpRetryPreset.TransportTransient);
139
- }
140
- if (typeof retry === "string") {
141
- if (!hasOwnValue(HttpRetryPreset, retry)) {
142
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${retry}`);
143
- }
144
- return createRetryOptions(retry);
145
- }
146
- if (typeof retry !== "object" || retry === null) {
147
- throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
148
- }
149
- if (Array.isArray(retry)) {
150
- throw createInvalidRetryPolicyError("HTTP retry policy must be a plain object");
151
- }
152
- validateRetryOptionsShape(retry);
153
- const base = createRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient);
154
- const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
155
- const normalized = {
156
- preset: retry.preset ?? base.preset,
157
- attempts: clampPositiveInteger(retry.attempts, base.attempts, MAX_RETRY_ATTEMPTS),
158
- methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
159
- statusCodes: retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
160
- base.statusCodes,
161
- errorCodes: retry.errorCodes ?? base.errorCodes,
162
- delayStrategy: retry.delayStrategy ?? base.delayStrategy,
163
- baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
164
- maxDelayMs,
165
- jitter: retry.jitter ?? base.jitter,
166
- retryAfter: retry.retryAfter ?? base.retryAfter,
167
- unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
168
- };
169
- return normalized;
170
- }
171
- function validateRetryOptionsShape(retry) {
172
- if (retry.preset !== undefined &&
173
- !hasOwnValue(HttpRetryPreset, retry.preset)) {
174
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${String(retry.preset)}`);
175
- }
176
- if (retry.delayStrategy !== undefined &&
177
- !hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)) {
178
- throw createInvalidRetryPolicyError(`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`);
179
- }
180
- if (retry.jitter !== undefined &&
181
- !hasOwnValue(HttpRetryJitter, retry.jitter)) {
182
- throw createInvalidRetryPolicyError(`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`);
183
- }
184
- if (retry.retryAfter !== undefined &&
185
- !hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
186
- throw createInvalidRetryPolicyError(`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`);
187
- }
188
- if (retry.unsafeMethodPolicy !== undefined &&
189
- !hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)) {
190
- throw createInvalidRetryPolicyError(`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`);
191
- }
192
- if (retry.methods !== undefined) {
193
- if (!Array.isArray(retry.methods)) {
194
- throw createInvalidRetryPolicyError("HTTP retry methods must be an array");
195
- }
196
- const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
197
- if (nonStringMethods.length > 0) {
198
- throw createInvalidRetryPolicyError("HTTP retry methods must contain only strings");
199
- }
200
- const unknownMethods = retry.methods
201
- .map((method) => method.toUpperCase())
202
- .filter((method) => !KNOWN_RETRY_METHODS.has(method));
203
- if (unknownMethods.length > 0) {
204
- throw createInvalidRetryPolicyError(`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`);
205
- }
206
- }
207
- if (retry.statusCodes !== undefined) {
208
- if (!Array.isArray(retry.statusCodes)) {
209
- throw createInvalidRetryPolicyError("HTTP retry statusCodes must be an array");
210
- }
211
- const invalidStatusCodes = retry.statusCodes.filter((status) => !Number.isInteger(status) ||
212
- Number(status) < 100 ||
213
- Number(status) > 599);
214
- if (invalidStatusCodes.length > 0) {
215
- throw createInvalidRetryPolicyError("HTTP retry statusCodes must contain HTTP status integers in [100, 599]");
216
- }
217
- }
218
- if (retry.errorCodes !== undefined) {
219
- if (!Array.isArray(retry.errorCodes)) {
220
- throw createInvalidRetryPolicyError("HTTP retry errorCodes must be an array");
221
- }
222
- const nonStringErrorCodes = retry.errorCodes.filter((errorCode) => typeof errorCode !== "string");
223
- if (nonStringErrorCodes.length > 0) {
224
- throw createInvalidRetryPolicyError("HTTP retry errorCodes must contain only strings");
225
- }
226
- }
227
- if (retry.preset === HttpRetryPreset.Off &&
228
- ((retry.attempts !== undefined && retry.attempts > 1) ||
229
- (retry.statusCodes !== undefined && retry.statusCodes.length > 0))) {
230
- throw createInvalidRetryPolicyError("HTTP retry preset off cannot be combined with retry-enabling overrides");
231
- }
232
- }
233
- function validateUnsafeRetryMethods(options) {
234
- if (options.unsafeMethodPolicy ===
235
- HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
236
- return;
237
- }
238
- const unsafeMethods = options.methods.filter((method) => UNSAFE_RETRY_METHODS.has(method.toUpperCase()));
239
- if (unsafeMethods.length === 0)
240
- return;
241
- throw new ProviderError(`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
242
- }
243
- function isMethodRetryable(method, options) {
244
- return options.methods
245
- .map((allowedMethod) => allowedMethod.toUpperCase())
246
- .includes(method.toUpperCase());
247
- }
248
- function retryErrorCode(error) {
249
- if (error instanceof TransportError) {
250
- return error.code;
251
- }
252
- if (error && typeof error === "object" && "code" in error) {
253
- const code = Reflect.get(error, "code");
254
- return typeof code === "string" ? code : undefined;
255
- }
256
- return undefined;
257
- }
258
- function retryErrorStatus(error) {
259
- if (error instanceof TransportError) {
260
- return error.status ?? error.upstreamStatus;
261
- }
262
- return undefined;
263
- }
264
- function shouldRetryTransportError(error, options) {
265
- const code = retryErrorCode(error);
266
- return Boolean(code && options.errorCodes.includes(code));
267
- }
268
- function retryAfterHeader(headers) {
269
- for (const [name, value] of Object.entries(headers)) {
270
- if (name.toLowerCase() === "retry-after")
271
- return value;
272
- }
273
- return undefined;
274
- }
275
- function parseRetryAfterMs(headers, now = Date.now()) {
276
- const value = retryAfterHeader(headers);
277
- if (!value)
278
- return undefined;
279
- const seconds = Number(value);
280
- if (Number.isFinite(seconds)) {
281
- return Math.max(0, Math.floor(seconds * 1_000));
282
- }
283
- const dateMs = Date.parse(value);
284
- if (!Number.isNaN(dateMs)) {
285
- return Math.max(0, dateMs - now);
286
- }
287
- return undefined;
288
- }
289
- function computeRetryDelayMs(options, attemptIndex, headers) {
290
- const multiplier = options.delayStrategy === HttpRetryDelayStrategy.Exponential
291
- ? 2 ** Math.max(0, attemptIndex - 1)
292
- : 1;
293
- const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
294
- const retryAfterMs = options.retryAfter === HttpRetryAfterPolicy.Ignore
295
- ? undefined
296
- : headers
297
- ? parseRetryAfterMs(headers)
298
- : undefined;
299
- if (retryAfterMs !== undefined) {
300
- const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
301
- if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
302
- return Math.min(boundedRetryAfterMs, configuredDelay);
303
- }
304
- return boundedRetryAfterMs;
305
- }
306
- switch (options.jitter) {
307
- case HttpRetryJitter.None:
308
- return configuredDelay;
309
- case HttpRetryJitter.Equal:
310
- return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
311
- case HttpRetryJitter.Full:
312
- return Math.floor(Math.random() * configuredDelay);
313
- }
314
- }
315
10
  async function sleep(ms) {
316
11
  if (ms <= 0)
317
12
  return;
@@ -330,9 +25,7 @@ function hasHeader(headers, name) {
330
25
  }
331
26
  function withClientHeaders(options, clientOptions, body) {
332
27
  const headers = {
333
- ...(clientOptions.userAgent
334
- ? { "User-Agent": clientOptions.userAgent }
335
- : {}),
28
+ ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
336
29
  ...options?.headers,
337
30
  };
338
31
  if (body !== undefined && !hasHeader(headers, "Content-Type")) {
@@ -344,9 +37,7 @@ function withClientHeaders(options, clientOptions, body) {
344
37
  };
345
38
  }
346
39
  function parseHttpData(body, headers) {
347
- const contentType = headers["content-type"] ??
348
- headers["Content-Type"] ??
349
- headers["CONTENT-TYPE"];
40
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
350
41
  if (contentType?.includes("application/json")) {
351
42
  return body ? JSON.parse(body) : null;
352
43
  }
@@ -396,22 +87,21 @@ function toHttpTransportError(error) {
396
87
  }
397
88
  async function toNativeHttpResponse(response) {
398
89
  const headers = Object.fromEntries(response.headers.entries());
399
- const rawText = await response.text();
90
+ const bodyBytes = new Uint8Array(await response.arrayBuffer());
91
+ const rawText = new TextDecoder().decode(bodyBytes);
400
92
  const data = parseHttpData(rawText, headers);
401
93
  return {
402
94
  data,
403
95
  headers,
404
96
  json: async () => {
405
- const contentType = headers["content-type"] ??
406
- headers["Content-Type"] ??
407
- headers["CONTENT-TYPE"];
408
- return parseJson(contentType?.includes("application/json") && !rawText
409
- ? "null"
410
- : rawText);
97
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
98
+ return parseJson(contentType?.includes("application/json") && !rawText ? "null" : rawText);
411
99
  },
412
100
  ok: response.status >= 200 && response.status < 300,
413
101
  status: response.status,
414
102
  text: async () => rawText,
103
+ arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
104
+ bytes: async () => bodyBytes.slice(0),
415
105
  };
416
106
  }
417
107
  async function drainNativeResponseBody(response) {
@@ -474,12 +164,17 @@ function isAbsoluteUrl(url) {
474
164
  function resolveHttpUrl(baseUrl, url) {
475
165
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
476
166
  }
477
- async function resolveNativeProxy(options, clientOptions, warn) {
167
+ async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
478
168
  const resolvedProxy = await resolveProxyConfigAsync({
479
169
  proxy: options.proxy ?? clientOptions.proxy,
480
170
  upstream: clientOptions.upstream,
481
171
  apifuseConfig: clientOptions.apifuseConfig,
172
+ proxyPolicy: clientOptions.proxyPolicy,
482
173
  affinityKey: clientOptions.affinityKey,
174
+ proxyAttempt: computeProxyAttemptIndex({
175
+ baseProxyAttempt: clientOptions.proxyAttempt,
176
+ retryAttemptOffset: proxyAttemptOffset,
177
+ }),
483
178
  telemetry: clientOptions.telemetry,
484
179
  });
485
180
  if (resolvedProxy.shouldWarn) {
@@ -501,14 +196,15 @@ function normalizeNativeFetchBody(body) {
501
196
  copied.set(normalized);
502
197
  return copied.buffer;
503
198
  }
504
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes) {
199
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
505
200
  const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
506
201
  const controller = options.timeout ? new AbortController() : undefined;
507
202
  const timeoutHandle = options.timeout
508
203
  ? setTimeout(() => controller?.abort(), options.timeout)
509
204
  : undefined;
205
+ let proxy;
510
206
  try {
511
- const proxy = await resolveNativeProxy(options, clientOptions, warn);
207
+ proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
512
208
  const requestInit = {
513
209
  headers: options.headers,
514
210
  method,
@@ -529,6 +225,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
529
225
  status: response.status,
530
226
  headers,
531
227
  retryable: statusRetryCodes.includes(response.status),
228
+ proxyUsed: Boolean(proxy),
532
229
  };
533
230
  }
534
231
  if (response.status >= 400 && options.throwOnHttpError !== false) {
@@ -544,7 +241,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
544
241
  if (error instanceof SyntaxError) {
545
242
  throw error;
546
243
  }
547
- throw toHttpTransportError(error);
244
+ const transportError = toHttpTransportError(error);
245
+ transportError.proxyUsed = Boolean(proxy);
246
+ throw transportError;
548
247
  }
549
248
  finally {
550
249
  if (timeoutHandle)
@@ -608,20 +307,25 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
608
307
  assertNoHttpTransportOverrides(options);
609
308
  const headersOptions = withClientHeaders(options, clientOptions, options.body);
610
309
  const methodName = normalizeHttpMethod(method);
611
- const retryOptions = normalizeRetryOptions(headersOptions.retry);
310
+ const explicitRetry = headersOptions.retry !== undefined;
311
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
312
+ label: "HTTP",
313
+ }) ??
314
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
612
315
  if (retryOptions)
613
- validateUnsafeRetryMethods(retryOptions);
316
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
614
317
  const retryEnabled = Boolean(retryOptions &&
615
318
  retryOptions.attempts > 1 &&
616
- isMethodRetryable(methodName, retryOptions));
319
+ isProxyTransportRetryMethod(methodName, retryOptions));
617
320
  const statusRetryEnabled = Boolean(retryEnabled &&
321
+ explicitRetry &&
618
322
  retryOptions &&
619
323
  retryOptions.statusCodes.length > 0 &&
620
324
  headersOptions.throwOnHttpError !== false);
621
325
  const attemptOptions = statusRetryEnabled
622
326
  ? { ...headersOptions, throwOnHttpError: false }
623
327
  : headersOptions;
624
- const executeOnce = () => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined);
328
+ const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
625
329
  if (!retryEnabled || !retryOptions) {
626
330
  const outcome = await executeOnce();
627
331
  if (isHttpStatusOutcome(outcome)) {
@@ -633,18 +337,17 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
633
337
  let lastStatus;
634
338
  for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
635
339
  try {
636
- const outcome = await executeOnce();
340
+ const outcome = await executeOnce(attempt - 1);
637
341
  if (isHttpStatusOutcome(outcome)) {
638
342
  lastStatus = outcome.status;
639
343
  if (outcome.retryable && attempt < retryOptions.attempts) {
640
- await sleep(computeRetryDelayMs(retryOptions, attempt, outcome.headers));
344
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
641
345
  continue;
642
346
  }
643
347
  throw toUpstreamHttpError(outcome.status);
644
348
  }
645
349
  const response = outcome;
646
- if (response.status >= 400 &&
647
- headersOptions.throwOnHttpError !== false) {
350
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
648
351
  throw toUpstreamHttpError(response.status);
649
352
  }
650
353
  if (attempt > 1) {
@@ -661,11 +364,18 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
661
364
  return response;
662
365
  }
663
366
  catch (error) {
664
- lastErrorCode = retryErrorCode(error);
665
- lastStatus = retryErrorStatus(error);
367
+ lastErrorCode = proxyTransportRetryErrorCode(error);
368
+ lastStatus = proxyTransportRetryErrorStatus(error);
369
+ const proxyUsed = Boolean(error.proxyUsed);
666
370
  if (attempt < retryOptions.attempts &&
667
- shouldRetryTransportError(error, retryOptions)) {
668
- await sleep(computeRetryDelayMs(retryOptions, attempt));
371
+ shouldRetryProxyTransportAttempt({
372
+ error,
373
+ explicitRetry,
374
+ method: methodName,
375
+ options: retryOptions,
376
+ proxyUsed,
377
+ })) {
378
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
669
379
  continue;
670
380
  }
671
381
  throw error;
@@ -0,0 +1,40 @@
1
+ import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types";
2
+ import { HttpRetryPreset } from "../types";
3
+ export type NormalizedProxyTransportRetryOptions = Required<Pick<HttpRetryOptions, "attempts" | "delayStrategy" | "baseDelayMs" | "maxDelayMs" | "jitter" | "retryAfter" | "unsafeMethodPolicy">> & {
4
+ preset?: HttpRetryPreset;
5
+ methods: readonly string[];
6
+ statusCodes: readonly number[];
7
+ errorCodes: readonly string[];
8
+ };
9
+ export declare const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS: readonly ["GET", "HEAD", "OPTIONS"];
10
+ export declare const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES: readonly ["transport_network_error", "transport_timeout"];
11
+ export declare const MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS = 8;
12
+ type RetryPolicyLabel = "HTTP" | "Stealth" | "Proxy transport";
13
+ export declare function proxyTransportRetryErrorCode(error: unknown): string | undefined;
14
+ export declare function proxyTransportRetryErrorStatus(error: unknown): number | undefined;
15
+ export declare function createDefaultProxyTransportRetryOptions(options?: {
16
+ extraErrorCodes?: readonly string[];
17
+ label?: RetryPolicyLabel;
18
+ }): NormalizedProxyTransportRetryOptions;
19
+ export declare function normalizeProxyTransportRetryOptions(retry: RequestOptions["retry"], options?: {
20
+ extraErrorCodes?: readonly string[];
21
+ label?: RetryPolicyLabel;
22
+ }): NormalizedProxyTransportRetryOptions | undefined;
23
+ export declare function validateUnsafeProxyTransportRetryMethods(options: NormalizedProxyTransportRetryOptions, label?: RetryPolicyLabel): void;
24
+ export declare function isProxyTransportRetryMethod(method: HttpMethod | string, options: NormalizedProxyTransportRetryOptions): boolean;
25
+ export declare function shouldRetryProxyTransportError(error: unknown, options: NormalizedProxyTransportRetryOptions): boolean;
26
+ export declare function shouldRetryProxyTransportAttempt(input: {
27
+ error: unknown;
28
+ explicitRetry: boolean;
29
+ method: HttpMethod | string;
30
+ options: NormalizedProxyTransportRetryOptions | undefined;
31
+ proxyUsed: boolean;
32
+ }): boolean;
33
+ export declare function computeProxyTransportRetryDelayMs(options: NormalizedProxyTransportRetryOptions, attemptIndex: number, headers?: Record<string, string>): number;
34
+ export declare function normalizeProxyAttemptIndex(value: number | undefined): number;
35
+ export declare function computeProxyAttemptIndex(options: {
36
+ baseProxyAttempt?: number;
37
+ proxyAttemptOffset?: number;
38
+ retryAttemptOffset?: number;
39
+ }): number;
40
+ export {};