Files
HealthLog/src/lib/http/read-capped.ts
T
Marc-André Bombeck 0f2b548138 style: apply prettier 3.9.1 formatting
Mechanical reflow from the prettier 3.9.1 bump; no behaviour change.
2026-06-28 19:26:16 +02:00

62 lines
2.0 KiB
TypeScript

/**
* Bounded body reader (M3).
*
* `await res.text()` / `await request.text()` buffer the WHOLE body before any
* size check runs, so a hostile peer can stream (or decompress) gigabytes into
* memory for the duration of the request timeout before a post-hoc
* `raw.length > MAX` rejection fires. This helper enforces the cap WHILE
* reading: it rejects up front on an oversized `Content-Length`, then streams
* the body and aborts the moment the running byte count exceeds the ceiling.
*
* Returns `{ ok: false }` on overflow (the caller maps it to its own error);
* returns the decoded UTF-8 text on success.
*/
export type CappedReadResult =
{ ok: true; text: string } | { ok: false; reason: "too_large" };
export async function readBodyCapped(
source: { headers: Headers; body: ReadableStream<Uint8Array> | null },
maxBytes: number,
): Promise<CappedReadResult> {
// Up-front reject on a declared oversized length — cheap and stops the read
// before a single byte is buffered.
const declared = source.headers.get("content-length");
if (declared) {
const n = Number(declared);
if (Number.isFinite(n) && n > maxBytes) {
return { ok: false, reason: "too_large" };
}
}
const body = source.body;
if (!body) return { ok: true, text: "" };
const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
total += value.byteLength;
if (total > maxBytes) {
// Stop pulling immediately — do not buffer past the ceiling.
await reader.cancel().catch(() => {});
return { ok: false, reason: "too_large" };
}
chunks.push(value);
}
} finally {
reader.releaseLock?.();
}
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
return { ok: true, text: new TextDecoder("utf-8").decode(merged) };
}