fix(web_fetch): cap response body before parsing

This commit is contained in:
Peter Steinberger
2026-02-16 01:19:04 +01:00
parent fd3d452f1f
commit 166cf6a3e0
7 changed files with 284 additions and 8 deletions

View File

@@ -86,10 +86,85 @@ export function withTimeout(signal: AbortSignal | undefined, timeoutMs: number):
return controller.signal;
}
export async function readResponseText(res: Response): Promise<string> {
export type ReadResponseTextResult = {
text: string;
truncated: boolean;
bytesRead: number;
};
export async function readResponseText(
res: Response,
options?: { maxBytes?: number },
): Promise<ReadResponseTextResult> {
const maxBytesRaw = options?.maxBytes;
const maxBytes =
typeof maxBytesRaw === "number" && Number.isFinite(maxBytesRaw) && maxBytesRaw > 0
? Math.floor(maxBytesRaw)
: undefined;
const body = (res as unknown as { body?: unknown }).body;
if (
maxBytes &&
body &&
typeof body === "object" &&
"getReader" in body &&
typeof (body as { getReader: () => unknown }).getReader === "function"
) {
const reader = (body as ReadableStream<Uint8Array>).getReader();
const decoder = new TextDecoder();
let bytesRead = 0;
let truncated = false;
const parts: string[] = [];
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
if (!value || value.byteLength === 0) {
continue;
}
let chunk = value;
if (bytesRead + chunk.byteLength > maxBytes) {
const remaining = Math.max(0, maxBytes - bytesRead);
if (remaining <= 0) {
truncated = true;
break;
}
chunk = chunk.subarray(0, remaining);
truncated = true;
}
bytesRead += chunk.byteLength;
parts.push(decoder.decode(chunk, { stream: true }));
if (truncated || bytesRead >= maxBytes) {
truncated = true;
break;
}
}
} catch {
// Best-effort: return whatever we decoded so far.
} finally {
if (truncated) {
try {
await reader.cancel();
} catch {
// ignore
}
}
}
parts.push(decoder.decode());
return { text: parts.join(""), truncated, bytesRead };
}
try {
return await res.text();
const text = await res.text();
return { text, truncated: false, bytesRead: text.length };
} catch {
return "";
return { text: "", truncated: false, bytesRead: 0 };
}
}