mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 15:18:09 +00:00
Reject unsafe decimal Content-Length values in the E2E bounded response text helper before streaming response bodies. Keep non-decimal values on the streaming byte-limit path and add regression coverage proving unsafe declared lengths cancel without starting a read. Proof: direct patched repro rejects before reading with code ETOOBIG; origin/main comparison entered the reader first; node --check scripts/e2e/lib/bounded-response-text.mjs; git diff --check origin/main...HEAD; autoreview clean overall 0.86; exact-head release gate succeeded at https://github.com/openclaw/openclaw/actions/runs/27846197115.
69 lines
1.9 KiB
JavaScript
69 lines
1.9 KiB
JavaScript
// Bounded response body reader used by E2E HTTP fixture clients.
|
|
function bodyTooLargeError(label, byteLimit) {
|
|
return Object.assign(new Error(`${label} response body exceeded ${byteLimit} bytes`), {
|
|
code: "ETOOBIG",
|
|
});
|
|
}
|
|
|
|
function cancelReaderSoon(reader) {
|
|
void Promise.resolve()
|
|
.then(() => reader.cancel())
|
|
.catch(() => {});
|
|
}
|
|
|
|
function parseContentLengthHeader(headers) {
|
|
const raw = headers.get("content-length");
|
|
if (!raw || !/^\d+$/u.test(raw)) {
|
|
return undefined;
|
|
}
|
|
const parsed = Number(raw);
|
|
return Number.isSafeInteger(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
export async function readBoundedResponseText(response, label, byteLimit, timeoutPromise) {
|
|
const contentLength = parseContentLengthHeader(response.headers);
|
|
if (contentLength !== undefined && contentLength > byteLimit) {
|
|
await response.body?.cancel().catch(() => {});
|
|
throw bodyTooLargeError(label, byteLimit);
|
|
}
|
|
if (!response.body) {
|
|
return "";
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let byteCount = 0;
|
|
let text = "";
|
|
let canceled = false;
|
|
try {
|
|
while (true) {
|
|
const read = reader.read();
|
|
const readWithTimeout = timeoutPromise
|
|
? Promise.race([
|
|
read,
|
|
timeoutPromise.catch((error) => {
|
|
canceled = true;
|
|
cancelReaderSoon(reader);
|
|
throw error;
|
|
}),
|
|
])
|
|
: read;
|
|
const { done, value } = await readWithTimeout;
|
|
if (done) {
|
|
return text + decoder.decode();
|
|
}
|
|
byteCount += value.byteLength;
|
|
if (byteCount > byteLimit) {
|
|
canceled = true;
|
|
await reader.cancel().catch(() => {});
|
|
throw bodyTooLargeError(label, byteLimit);
|
|
}
|
|
text += decoder.decode(value, { stream: true });
|
|
}
|
|
} finally {
|
|
if (!canceled) {
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
}
|