export class ResponseBodyTooLargeError extends Error { constructor(maxBytes: number) { super(`Response body exceeds ${maxBytes} bytes`); this.name = "ResponseBodyTooLargeError"; } } export async function readBoundedJsonResponse( response: Response, maxBytes: number, signal?: AbortSignal, ): Promise { if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) { throw new Error("Response body limit must be a positive integer"); } const declared = Number(response.headers.get("content-length") ?? Number.NaN); if (Number.isFinite(declared) && declared > maxBytes) { cancelResponseBody(response); throw new ResponseBodyTooLargeError(maxBytes); } if (!response.body) return JSON.parse("") as T; const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let total = 0; try { for (;;) { const { done, value } = await readWithAbort(reader, signal); if (done) break; if (!value?.byteLength) continue; total += value.byteLength; if (total > maxBytes) { cancelReader(reader); throw new ResponseBodyTooLargeError(maxBytes); } chunks.push(value); } } catch (error) { cancelReader(reader); throw error; } finally { try { reader.releaseLock(); } catch { // The reader may already be cancelled or released. } } const bytes = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } return JSON.parse(new TextDecoder().decode(bytes)) as T; } function readWithAbort( reader: ReadableStreamDefaultReader, signal?: AbortSignal, ): ReturnType["read"]> { if (!signal) return reader.read(); if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Request aborted")); return new Promise((resolve, reject) => { const onAbort = () => { cancelReader(reader); reject(signal.reason ?? new Error("Request aborted")); }; signal.addEventListener("abort", onAbort, { once: true }); reader.read().then( (result) => { signal.removeEventListener("abort", onAbort); resolve(result); }, (error: unknown) => { signal.removeEventListener("abort", onAbort); reject(error); }, ); }); } export function cancelResponseBody(response: Response): void { try { void Promise.resolve(response.body?.cancel()).catch(() => undefined); } catch { // Cancellation is best-effort and must not delay the bounded failure. } } function cancelReader(reader: ReadableStreamDefaultReader): void { try { void Promise.resolve(reader.cancel()).catch(() => undefined); } catch { // Cancellation is best-effort and must not delay the bounded failure. } }