fetchCore reads the whole body of any non-2xx response into memory at src/transport/httpFetch.ts:128:
const errorBody = await finalResponse.text().catch(() => '');
No cap applies. The byte limits callers pass are enforced on the success path only, and node-fetch's own size option is left at its default of 0, which means unlimited. So a source that answers a rejected request with a multi-megabyte error page — a gateway HTML dump, a stack trace, a debug listing — spikes the heap by the full body size, however small a limit the caller asked for.
The destination is caller-supplied in every *-from-url tool, and the guard that stops those URLs reaching private addresses does not bound what a public one returns.
Reproduced against a loopback server answering 500 with a 5 MB body, while asking for a 3-byte cap:
PROBE 2 (non-2xx body read):
status: 500 | bytes buffered: 5000000
The whole body is buffered to build a message that quotes a few hundred characters of it.
AI-authored — Claude Code, Opus 5 (xhigh); filed at @alistair3149's request following a code review; not yet human-reviewed; reproduced locally against a loopback server, output above.
fetchCorereads the whole body of any non-2xx response into memory atsrc/transport/httpFetch.ts:128:No cap applies. The byte limits callers pass are enforced on the success path only, and node-fetch's own
sizeoption is left at its default of 0, which means unlimited. So a source that answers a rejected request with a multi-megabyte error page — a gateway HTML dump, a stack trace, a debug listing — spikes the heap by the full body size, however small a limit the caller asked for.The destination is caller-supplied in every
*-from-urltool, and the guard that stops those URLs reaching private addresses does not bound what a public one returns.Reproduced against a loopback server answering
500with a 5 MB body, while asking for a 3-byte cap:The whole body is buffered to build a message that quotes a few hundred characters of it.