mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
62 lines
2.0 KiB
TypeScript
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) };
|
|
}
|