@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.
- package/LICENSE +661 -0
- package/dist/asset-block-measure.d.ts +14 -0
- package/dist/asset-block-measure.js +82 -0
- package/dist/asset-block-measure.js.map +1 -0
- package/dist/browser-pool.d.ts +65 -0
- package/dist/browser-pool.js +161 -0
- package/dist/browser-pool.js.map +1 -0
- package/dist/cost.d.ts +51 -0
- package/dist/cost.js +54 -0
- package/dist/cost.js.map +1 -0
- package/dist/fetcher-brightdata.d.ts +52 -0
- package/dist/fetcher-brightdata.js +135 -0
- package/dist/fetcher-brightdata.js.map +1 -0
- package/dist/fetcher-escalating.d.ts +100 -0
- package/dist/fetcher-escalating.js +489 -0
- package/dist/fetcher-escalating.js.map +1 -0
- package/dist/fetcher-http.d.ts +81 -0
- package/dist/fetcher-http.js +395 -0
- package/dist/fetcher-http.js.map +1 -0
- package/dist/fetcher-patchright.d.ts +212 -0
- package/dist/fetcher-patchright.js +505 -0
- package/dist/fetcher-patchright.js.map +1 -0
- package/dist/fetcher-robots-gate.d.ts +60 -0
- package/dist/fetcher-robots-gate.js +87 -0
- package/dist/fetcher-robots-gate.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-options.d.ts +59 -0
- package/dist/internal-options.js +16 -0
- package/dist/internal-options.js.map +1 -0
- package/dist/perf-harness.d.ts +105 -0
- package/dist/perf-harness.js +176 -0
- package/dist/perf-harness.js.map +1 -0
- package/dist/storage-memory.d.ts +18 -0
- package/dist/storage-memory.js +45 -0
- package/dist/storage-memory.js.map +1 -0
- package/dist/storage-r2.d.ts +44 -0
- package/dist/storage-r2.js +209 -0
- package/dist/storage-r2.js.map +1 -0
- package/dist/types.d.ts +231 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- 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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"}
|