@bluefields/fetcher 0.2.0

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 (44) hide show
  1. package/LICENSE +661 -0
  2. package/dist/asset-block-measure.d.ts +14 -0
  3. package/dist/asset-block-measure.js +82 -0
  4. package/dist/asset-block-measure.js.map +1 -0
  5. package/dist/browser-pool.d.ts +65 -0
  6. package/dist/browser-pool.js +161 -0
  7. package/dist/browser-pool.js.map +1 -0
  8. package/dist/cost.d.ts +51 -0
  9. package/dist/cost.js +54 -0
  10. package/dist/cost.js.map +1 -0
  11. package/dist/fetcher-brightdata.d.ts +52 -0
  12. package/dist/fetcher-brightdata.js +135 -0
  13. package/dist/fetcher-brightdata.js.map +1 -0
  14. package/dist/fetcher-escalating.d.ts +100 -0
  15. package/dist/fetcher-escalating.js +489 -0
  16. package/dist/fetcher-escalating.js.map +1 -0
  17. package/dist/fetcher-http.d.ts +81 -0
  18. package/dist/fetcher-http.js +395 -0
  19. package/dist/fetcher-http.js.map +1 -0
  20. package/dist/fetcher-patchright.d.ts +212 -0
  21. package/dist/fetcher-patchright.js +505 -0
  22. package/dist/fetcher-patchright.js.map +1 -0
  23. package/dist/fetcher-robots-gate.d.ts +60 -0
  24. package/dist/fetcher-robots-gate.js +87 -0
  25. package/dist/fetcher-robots-gate.js.map +1 -0
  26. package/dist/index.d.ts +93 -0
  27. package/dist/index.js +219 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/internal-options.d.ts +59 -0
  30. package/dist/internal-options.js +16 -0
  31. package/dist/internal-options.js.map +1 -0
  32. package/dist/perf-harness.d.ts +105 -0
  33. package/dist/perf-harness.js +176 -0
  34. package/dist/perf-harness.js.map +1 -0
  35. package/dist/storage-memory.d.ts +18 -0
  36. package/dist/storage-memory.js +45 -0
  37. package/dist/storage-memory.js.map +1 -0
  38. package/dist/storage-r2.d.ts +44 -0
  39. package/dist/storage-r2.js +209 -0
  40. package/dist/storage-r2.js.map +1 -0
  41. package/dist/types.d.ts +231 -0
  42. package/dist/types.js +22 -0
  43. package/dist/types.js.map +1 -0
  44. package/package.json +54 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * HTTP fetcher adapter (chunk 8).
3
+ *
4
+ * Default v1 adapter for environments without Patchright + IPRoyal. Uses
5
+ * the global fetch with the SSRF-safe undici dispatcher from @bluefields/primitives.
6
+ * Fingerprint diversity, proxy rotation, browser-level stealth are NOT
7
+ * implemented here — that's `fetcher-patchright.ts` (env-gated, requires
8
+ * the Patchright dep + IPRoyal credentials to come online).
9
+ *
10
+ * The egressIp returned is the local outbound IP for direct fetches
11
+ * (we report '0.0.0.0' as a placeholder since the platform doesn't see
12
+ * its own egress IP without an external service call). When the
13
+ * Patchright adapter ships, egressIp will be the proxy's egress.
14
+ */
15
+ import type { FetcherAdapter, RequestCookie } from './types.js';
16
+ /** True for 403/429/503 — statuses whose body can't change the escalate decision (see L1). */
17
+ export declare function isDecisiveBlockStatus(status: number): boolean;
18
+ export interface HttpFetcherOptions {
19
+ /** Test seam: override the HTTP fetch. */
20
+ fetchImpl?: typeof fetch;
21
+ /**
22
+ * Hard cap (bytes) on the response body buffered into memory; an oversize body
23
+ * is rejected rather than read in full. Default `DEFAULT_MAX_BODY_BYTES` (5 MiB).
24
+ */
25
+ maxBodyBytes?: number;
26
+ }
27
+ /**
28
+ * Filter cookies that apply to `url` (domain + path match) and serialize
29
+ * them into a single `Cookie: a=1; b=2` header value.
30
+ *
31
+ * Domain match: cookie has no domain → applies; otherwise the request
32
+ * host must equal the cookie domain or be a subdomain of it (leading dot
33
+ * is tolerated; we trim it on compare).
34
+ *
35
+ * Path match: cookie has no path → applies; otherwise the request path
36
+ * must start with the cookie path.
37
+ *
38
+ * Expired cookies are dropped.
39
+ */
40
+ export declare function serializeCookiesForRequest(cookies: RequestCookie[], url: string): string;
41
+ export declare function createHttpFetcherAdapter(opts?: HttpFetcherOptions): FetcherAdapter;
42
+ export interface ReadBodyOptions {
43
+ /** Absolute deadline (Date.now ms) for the whole body read. Omit for no time bound. */
44
+ deadlineMs?: number;
45
+ /** Test seam: clock. Default Date.now. */
46
+ nowMsFn?: () => number;
47
+ }
48
+ /**
49
+ * Read a response body to a string under a hard byte cap AND a hard time budget,
50
+ * instead of buffering an unbounded / slow-streaming body. A cheap Content-Length
51
+ * reject, then a streaming read with a running byte counter; the whole drain is
52
+ * raced against `deadlineMs` so a slow-trickling or hung body is bounded even
53
+ * when the request's abort signal does NOT cancel an in-flight read on the
54
+ * underlying fetch — the real-undici case the perf benchmark exposed (a 1-credit
55
+ * tier-0 fetch running 90s despite an 8s abort). On cap/timeout we cancel the
56
+ * stream and throw a plain Error — the route maps it via its existing
57
+ * `upstream_fetch_failed` branch, so no new customer-facing error shape. Decodes
58
+ * UTF-8, identical to `Response.text()` (always UTF-8-decodes per the Fetch spec).
59
+ *
60
+ * Not an SSRF concern: it runs on an already-validated, already-fetched response.
61
+ * Exported for testing.
62
+ */
63
+ export declare function readBodyCapped(res: Response, maxBytes: number, opts?: ReadBodyOptions): Promise<string>;
64
+ /**
65
+ * Retry on transient network errors with full-jittered exponential backoff
66
+ * (base 200ms). Successful HTTP responses (including 4xx/5xx) pass through to
67
+ * the caller — 4xx is permanent, 5xx is retried only once because most 5xx
68
+ * pages are deliberate (rate limit, maintenance).
69
+ *
70
+ * Exported for testing.
71
+ */
72
+ export declare function fetchWithRetry(fetchImpl: typeof fetch, url: string, init: RequestInit, maxAttempts?: number): Promise<Response>;
73
+ /**
74
+ * Full-jitter exponential backoff: a uniform random delay in [0, base) where
75
+ * base = BACKOFF_BASE_MS · 2^attempt. The jitter de-synchronizes retries so a
76
+ * fleet hammering one flaky origin doesn't retry in lockstep (thundering herd).
77
+ * `rand` is injectable for tests; Math.random is fine — not security-sensitive.
78
+ *
79
+ * Exported for testing.
80
+ */
81
+ export declare function backoffMs(attempt: number, rand?: () => number): number;
@@ -0,0 +1,395 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * HTTP fetcher adapter (chunk 8).
4
+ *
5
+ * Default v1 adapter for environments without Patchright + IPRoyal. Uses
6
+ * the global fetch with the SSRF-safe undici dispatcher from @bluefields/primitives.
7
+ * Fingerprint diversity, proxy rotation, browser-level stealth are NOT
8
+ * implemented here — that's `fetcher-patchright.ts` (env-gated, requires
9
+ * the Patchright dep + IPRoyal credentials to come online).
10
+ *
11
+ * The egressIp returned is the local outbound IP for direct fetches
12
+ * (we report '0.0.0.0' as a placeholder since the platform doesn't see
13
+ * its own egress IP without an external service call). When the
14
+ * Patchright adapter ships, egressIp will be the proxy's egress.
15
+ */
16
+ import { envInt, ssrfSafeFetch, validateUrlForExternalRequest } from '@bluefields/primitives';
17
+ const DEFAULT_TIMEOUT_MS = envInt('BLUEFIELDS_HTTP_TIMEOUT_MS', 30_000);
18
+ const DEFAULT_UA = 'BlueFields-Fetcher/1.0 (+https://bluefields.dev)';
19
+ const FETCHER_ID = 'http-v1';
20
+ /** Placeholder when running without a proxy — real value lands with Patchright. */
21
+ const NO_PROXY_PLACEHOLDER_IP = '0.0.0.0';
22
+ /**
23
+ * Below this raw-HTML size, a `Set-Cookie` response is likely a consent /
24
+ * redirect interstitial — worth one cookie'd retry on the same URL before we'd
25
+ * escalate to the browser tier. Conservative so normal small pages rarely retry.
26
+ */
27
+ const CONSENT_RETRY_MAX_HTML = 8_192;
28
+ /**
29
+ * Hard cap on the response body we buffer into memory. Without it a
30
+ * pathologically large page is read in full before we ever measure it; past
31
+ * this many bytes we stop reading and reject. The rejection is a plain Error,
32
+ * so the route surfaces it through its existing `upstream_fetch_failed` mapping
33
+ * (no new customer-facing error shape). Override per-adapter via
34
+ * `HttpFetcherOptions.maxBodyBytes`.
35
+ */
36
+ const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MiB
37
+ /** Base unit for exponential backoff — doubled per attempt, then full-jittered. */
38
+ const BACKOFF_BASE_MS = 200;
39
+ /**
40
+ * Statuses on which tier-0's body cannot change the escalate decision (perf round, L1).
41
+ * 403/429/503 are unconditionally escalation-worthy: classifyFetchResult
42
+ * (`fetcher-escalating.ts`) maps them to `challenge` or `blocked`, both of which
43
+ * satisfy `needsEscalation()`. So when the escalating layer opts in via
44
+ * `__earlyClassify`, the HTTP adapter can skip draining the doomed body on these
45
+ * statuses and let escalation start at time-to-headers. Deliberately kept in sync with
46
+ * the status branch of `classifyFetchResult` (403/429/503).
47
+ */
48
+ const DECISIVE_BLOCK_STATUSES = new Set([403, 429, 503]);
49
+ /** True for 403/429/503 — statuses whose body can't change the escalate decision (see L1). */
50
+ export function isDecisiveBlockStatus(status) {
51
+ return DECISIVE_BLOCK_STATUSES.has(status);
52
+ }
53
+ /**
54
+ * Filter cookies that apply to `url` (domain + path match) and serialize
55
+ * them into a single `Cookie: a=1; b=2` header value.
56
+ *
57
+ * Domain match: cookie has no domain → applies; otherwise the request
58
+ * host must equal the cookie domain or be a subdomain of it (leading dot
59
+ * is tolerated; we trim it on compare).
60
+ *
61
+ * Path match: cookie has no path → applies; otherwise the request path
62
+ * must start with the cookie path.
63
+ *
64
+ * Expired cookies are dropped.
65
+ */
66
+ export function serializeCookiesForRequest(cookies, url) {
67
+ let host;
68
+ let pathname;
69
+ let isHttps;
70
+ try {
71
+ const u = new URL(url);
72
+ host = u.hostname.toLowerCase();
73
+ pathname = u.pathname || '/';
74
+ isHttps = u.protocol === 'https:';
75
+ }
76
+ catch {
77
+ return '';
78
+ }
79
+ const nowSec = Math.floor(Date.now() / 1000);
80
+ const matches = [];
81
+ for (const c of cookies) {
82
+ if (c.expires && c.expires < nowSec)
83
+ continue;
84
+ if (c.secure && !isHttps)
85
+ continue;
86
+ if (c.domain) {
87
+ const dom = c.domain.toLowerCase().replace(/^\./, '');
88
+ if (host !== dom && !host.endsWith(`.${dom}`))
89
+ continue;
90
+ }
91
+ if (c.path && !pathname.startsWith(c.path))
92
+ continue;
93
+ matches.push(`${c.name}=${c.value}`);
94
+ }
95
+ return matches.join('; ');
96
+ }
97
+ export function createHttpFetcherAdapter(opts = {}) {
98
+ // Tests inject opts.fetchImpl; production uses ssrfSafeFetch which
99
+ // routes through undici's fetch (not Node's global fetch — that
100
+ // silently ignores the dispatcher field on RequestInit and bypasses
101
+ // the SSRF lookup entirely).
102
+ const fetchImpl = (opts.fetchImpl ?? ssrfSafeFetch);
103
+ const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
104
+ return {
105
+ async fetch(url, options) {
106
+ if (options?.actions && options.actions.length > 0) {
107
+ throw new Error('HTTP fetcher cannot run page actions; use the Patchright fetcher');
108
+ }
109
+ await validateUrlForExternalRequest(url);
110
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
111
+ const userAgent = options?.userAgent ?? DEFAULT_UA;
112
+ const now = options?.nowFn ?? (() => new Date());
113
+ const controller = new AbortController();
114
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
115
+ const startedAt = Date.now();
116
+ const fetchedAt = now();
117
+ // Honest browser-parity content-negotiation + Fetch-Metadata headers.
118
+ // NOT impersonation: there is no browser-brand claim — the UA stays our
119
+ // honest token and we deliberately omit `sec-ch-ua`. These are standard
120
+ // request headers a plain fetch omits that many origins gate real content
121
+ // on, so adding them lifts tier-0 success and cuts escalation to the
122
+ // 3-/8-credit tiers. (UA kept honest, 2026-06-15; robots
123
+ // matching still keys on our product token. `accept-encoding` is left to
124
+ // undici, which sends gzip/deflate/br and auto-decompresses.)
125
+ const headers = {
126
+ 'user-agent': userAgent,
127
+ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
128
+ 'accept-language': 'en-US,en;q=0.9',
129
+ 'sec-fetch-site': 'none',
130
+ 'sec-fetch-mode': 'navigate',
131
+ 'sec-fetch-user': '?1',
132
+ 'sec-fetch-dest': 'document',
133
+ 'upgrade-insecure-requests': '1',
134
+ };
135
+ // Caller headers override the defaults; UA from caller wins over the
136
+ // top-level userAgent option (rare but documented in types.ts).
137
+ if (options?.headers) {
138
+ for (const [k, v] of Object.entries(options.headers)) {
139
+ headers[k.toLowerCase()] = v;
140
+ }
141
+ }
142
+ // Browser-style cookies → single `Cookie:` request header. Filter by
143
+ // domain/path so a session save from foo.com doesn't leak its cookies
144
+ // when re-used against bar.com.
145
+ if (options?.cookies && options.cookies.length > 0) {
146
+ const cookieHeader = serializeCookiesForRequest(options.cookies, url);
147
+ if (cookieHeader)
148
+ headers.cookie = cookieHeader;
149
+ }
150
+ let res;
151
+ let html;
152
+ try {
153
+ // ssrfSafeFetch (or the injected fetchImpl) already wires the
154
+ // SSRF-safe dispatcher — no dispatcher field needed here.
155
+ // Retries: up to 3 attempts with full-jittered exponential backoff
156
+ // (base 200ms) for transient errors (network blips, 5xx). 4xx
157
+ // short-circuits — no amount of retrying fixes a 404.
158
+ res = await fetchWithRetry(fetchImpl, url, {
159
+ method: 'GET',
160
+ headers,
161
+ signal: controller.signal,
162
+ redirect: 'follow',
163
+ });
164
+ // Bound the body read by the SAME deadline as the fetch (startedAt +
165
+ // timeoutMs). The read-vs-deadline race in readBodyCapped is the real
166
+ // bound — the abort signal alone doesn't reliably cancel an in-flight
167
+ // undici body read (the perf benchmark showed 1-credit tier-0 fetches
168
+ // running 90-130s past their timeout). Keeping the read inside the
169
+ // clearTimeout window is belt-and-suspenders for the headers/connect phase.
170
+ if (options?.__earlyClassify &&
171
+ isDecisiveBlockStatus(res.status)) {
172
+ // Tier-0 early-block escalation (L1): 403/429/503 are unconditionally
173
+ // escalation-worthy upstream and the body cannot change that decision (only
174
+ // its challenge-vs-blocked LABEL, which escalates either way). Skip the doomed
175
+ // drain so the escalating layer escalates at ~time-to-headers instead of after
176
+ // the full (often slow-rolled) block body or the tier-0 fast-fail wall. Cancel
177
+ // the stream so undici frees the socket. Coverage-safe: html:'' on these
178
+ // statuses re-classifies to `blocked`, which still escalates identically.
179
+ await res.body?.cancel().catch(() => undefined);
180
+ html = '';
181
+ }
182
+ else {
183
+ html = await readBodyCapped(res, maxBodyBytes, { deadlineMs: startedAt + timeoutMs });
184
+ }
185
+ }
186
+ finally {
187
+ clearTimeout(timeout);
188
+ }
189
+ // Simple consent-cookie case: a thin 2xx that sets cookies is often a
190
+ // consent / redirect interstitial. Re-fetch the SAME URL once with those
191
+ // cookies — cheaper than escalating to the browser tier just to clear a
192
+ // consent wall. Best-effort (any failure keeps the first response) and
193
+ // skipped when the caller already supplied cookies. SSRF is re-validated
194
+ // on the retry URL — never weakened.
195
+ if (!options?.cookies?.length &&
196
+ res.status >= 200 &&
197
+ res.status < 300 &&
198
+ html.length < CONSENT_RETRY_MAX_HTML) {
199
+ const setCookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
200
+ const cookieHeader = setCookies
201
+ .map((c) => c.split(';')[0]?.trim())
202
+ .filter((c) => !!c)
203
+ .join('; ');
204
+ if (cookieHeader) {
205
+ const retryUrl = res.url || url;
206
+ try {
207
+ await validateUrlForExternalRequest(retryUrl);
208
+ const retryStartedAt = Date.now();
209
+ const retryController = new AbortController();
210
+ const retryTimer = setTimeout(() => retryController.abort(), timeoutMs);
211
+ try {
212
+ const res2 = await fetchWithRetry(fetchImpl, retryUrl, {
213
+ method: 'GET',
214
+ headers: { ...headers, cookie: cookieHeader },
215
+ signal: retryController.signal,
216
+ redirect: 'follow',
217
+ });
218
+ const html2 = await readBodyCapped(res2, maxBodyBytes, {
219
+ deadlineMs: retryStartedAt + timeoutMs,
220
+ });
221
+ // Keep the retry only if it actually returned more content.
222
+ if (html2.length > html.length) {
223
+ html = html2;
224
+ res = res2;
225
+ }
226
+ }
227
+ finally {
228
+ clearTimeout(retryTimer);
229
+ }
230
+ }
231
+ catch {
232
+ // best-effort — keep the first response.
233
+ }
234
+ }
235
+ }
236
+ const fetchDurationMs = Math.max(1, Date.now() - startedAt);
237
+ const responseHeaders = {};
238
+ res.headers.forEach((value, key) => {
239
+ responseHeaders[key.toLowerCase()] = value;
240
+ });
241
+ return {
242
+ html,
243
+ responseStatus: res.status,
244
+ responseHeaders,
245
+ fetchedAt,
246
+ fetchDurationMs,
247
+ fetcherId: FETCHER_ID,
248
+ proxyEgressIp: NO_PROXY_PLACEHOLDER_IP,
249
+ userAgent,
250
+ rawHtmlSizeBytes: Buffer.byteLength(html, 'utf8'),
251
+ // HTTP adapter can't render → no screenshot possible. If the
252
+ // caller needs one, they must route to a browser adapter (Patchright).
253
+ screenshot: null,
254
+ // finalUrl after redirects. fetch() follows redirects by default
255
+ // and exposes res.url with the final URL.
256
+ finalUrl: res.url || url,
257
+ };
258
+ },
259
+ };
260
+ }
261
+ /**
262
+ * Read a response body to a string under a hard byte cap AND a hard time budget,
263
+ * instead of buffering an unbounded / slow-streaming body. A cheap Content-Length
264
+ * reject, then a streaming read with a running byte counter; the whole drain is
265
+ * raced against `deadlineMs` so a slow-trickling or hung body is bounded even
266
+ * when the request's abort signal does NOT cancel an in-flight read on the
267
+ * underlying fetch — the real-undici case the perf benchmark exposed (a 1-credit
268
+ * tier-0 fetch running 90s despite an 8s abort). On cap/timeout we cancel the
269
+ * stream and throw a plain Error — the route maps it via its existing
270
+ * `upstream_fetch_failed` branch, so no new customer-facing error shape. Decodes
271
+ * UTF-8, identical to `Response.text()` (always UTF-8-decodes per the Fetch spec).
272
+ *
273
+ * Not an SSRF concern: it runs on an already-validated, already-fetched response.
274
+ * Exported for testing.
275
+ */
276
+ export async function readBodyCapped(res, maxBytes, opts = {}) {
277
+ const declared = Number(res.headers.get('content-length'));
278
+ if (Number.isFinite(declared) && declared > maxBytes) {
279
+ await res.body?.cancel().catch(() => undefined);
280
+ throw new Error(`response body exceeds ${maxBytes}-byte cap (declared ${declared})`);
281
+ }
282
+ const body = res.body;
283
+ if (!body)
284
+ return '';
285
+ const reader = body.getReader();
286
+ const drain = async () => {
287
+ const chunks = [];
288
+ let total = 0;
289
+ let chunk = await reader.read();
290
+ while (!chunk.done) {
291
+ if (chunk.value) {
292
+ total += chunk.value.byteLength;
293
+ if (total > maxBytes) {
294
+ throw new Error(`response body exceeds ${maxBytes}-byte cap`);
295
+ }
296
+ chunks.push(chunk.value);
297
+ }
298
+ chunk = await reader.read();
299
+ }
300
+ return Buffer.concat(chunks).toString('utf8');
301
+ };
302
+ const now = opts.nowMsFn ?? Date.now;
303
+ let timer;
304
+ try {
305
+ if (opts.deadlineMs === undefined) {
306
+ return await drain();
307
+ }
308
+ const remaining = Math.max(0, opts.deadlineMs - now());
309
+ // Race the whole drain against the time budget. A slow trickle (many quick
310
+ // reads over a long total) OR a single hung read both trip this — unlike the
311
+ // fetch abort signal, which the real undici body stream may ignore mid-read.
312
+ const timeout = new Promise((_, reject) => {
313
+ timer = setTimeout(() => reject(new Error(`response body read exceeded ${remaining}ms deadline`)), remaining);
314
+ });
315
+ return await Promise.race([drain(), timeout]);
316
+ }
317
+ finally {
318
+ if (timer)
319
+ clearTimeout(timer);
320
+ // Cancel the underlying download (stops a still-pending read on timeout);
321
+ // swallow any error. No-op once the body has drained.
322
+ await reader.cancel().catch(() => undefined);
323
+ }
324
+ }
325
+ /**
326
+ * Retryable network error codes — single transient blips on these
327
+ * shouldn't fail a customer-facing fetch.
328
+ */
329
+ const RETRYABLE_CAUSE_CODES = new Set([
330
+ 'ETIMEDOUT',
331
+ 'ECONNRESET',
332
+ 'ECONNREFUSED',
333
+ 'ENOTFOUND',
334
+ 'EAI_AGAIN',
335
+ 'UND_ERR_SOCKET',
336
+ 'UND_ERR_CONNECT_TIMEOUT',
337
+ 'UND_ERR_HEADERS_TIMEOUT',
338
+ ]);
339
+ /**
340
+ * Retry on transient network errors with full-jittered exponential backoff
341
+ * (base 200ms). Successful HTTP responses (including 4xx/5xx) pass through to
342
+ * the caller — 4xx is permanent, 5xx is retried only once because most 5xx
343
+ * pages are deliberate (rate limit, maintenance).
344
+ *
345
+ * Exported for testing.
346
+ */
347
+ export async function fetchWithRetry(fetchImpl, url, init, maxAttempts = 3) {
348
+ let lastError;
349
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
350
+ try {
351
+ const res = await fetchImpl(url, init);
352
+ // Retry 5xx once at attempt 0 (might be a transient origin blip).
353
+ if (res.status >= 500 && res.status < 600 && attempt === 0) {
354
+ await sleep(backoffMs(attempt));
355
+ continue;
356
+ }
357
+ return res;
358
+ }
359
+ catch (err) {
360
+ lastError = err;
361
+ if (!isRetryable(err) || attempt === maxAttempts - 1)
362
+ throw err;
363
+ await sleep(backoffMs(attempt));
364
+ }
365
+ }
366
+ throw lastError;
367
+ }
368
+ function isRetryable(err) {
369
+ if (!err || typeof err !== 'object')
370
+ return false;
371
+ const cause = err.cause;
372
+ const code = cause?.code ?? err.code;
373
+ if (typeof code === 'string' && RETRYABLE_CAUSE_CODES.has(code))
374
+ return true;
375
+ // Network errors from undici sometimes surface as "fetch failed" with
376
+ // a cause.code we missed. Retry one more time on any TypeError; if it
377
+ // was a programmer bug, the second attempt fails identically.
378
+ return err.name === 'TypeError';
379
+ }
380
+ /**
381
+ * Full-jitter exponential backoff: a uniform random delay in [0, base) where
382
+ * base = BACKOFF_BASE_MS · 2^attempt. The jitter de-synchronizes retries so a
383
+ * fleet hammering one flaky origin doesn't retry in lockstep (thundering herd).
384
+ * `rand` is injectable for tests; Math.random is fine — not security-sensitive.
385
+ *
386
+ * Exported for testing.
387
+ */
388
+ export function backoffMs(attempt, rand = Math.random) {
389
+ const base = BACKOFF_BASE_MS * 2 ** attempt;
390
+ return Math.floor(rand() * base);
391
+ }
392
+ function sleep(ms) {
393
+ return new Promise((resolve) => setTimeout(resolve, ms));
394
+ }
395
+ //# sourceMappingURL=fetcher-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher-http.js","sourceRoot":"","sources":["../src/fetcher-http.ts"],"names":[],"mappings":"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"}