/** * Read a response body up to `maxBytes`, or refuse it. * * `res.arrayBuffer()` and `res.json()` buffer however much the server sends before any * size check can run, and a mint is an untrusted server: the abort timer bounds how * *long* a read may take, this bounds how *large* it may get, and both are needed. The * Content-Length header is checked first as a courtesy — a chunked or lying response * still hits the streaming cap. * * Returns null when the body is over the limit or unreadable. The caller treats that * exactly like a failed fetch. */ export async function readBodyBounded(res: Response, maxBytes: number): Promise { const declared = Number(res.headers.get('content-length') ?? ''); if (Number.isFinite(declared) && declared > maxBytes) return null; const reader = res.body?.getReader(); if (!reader) return null; const chunks: Uint8Array[] = []; let size = 0; try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > maxBytes) { await reader.cancel().catch(() => undefined); return null; } chunks.push(value); } } catch { // Aborted by the caller's timer, or the connection died mid-body. return null; } return Buffer.concat(chunks); }