mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Supersedes #350 by @sendmebits, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Co-authored-by: sendmebits <[email protected]>
119 lines
3.7 KiB
JavaScript
119 lines
3.7 KiB
JavaScript
/**
|
|
* Read a fetch() Response body as text with a hard byte cap. Rejects early on an
|
|
* oversized Content-Length, then streams with a running cap so a chunked or
|
|
* length-omitted response cannot blow past the limit. Throws { code:'RESPONSE_TOO_LARGE' }.
|
|
*/
|
|
export async function readResponseTextCapped(response, maxBytes, signal) {
|
|
const tooLarge = () =>
|
|
Object.assign(new Error('Upstream response too large'), {
|
|
code: 'RESPONSE_TOO_LARGE',
|
|
});
|
|
const declared = Number(response.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
void response.body?.cancel().catch(() => {});
|
|
throw tooLarge();
|
|
}
|
|
signal?.throwIfAborted();
|
|
const reader = response.body?.getReader?.();
|
|
if (!reader) {
|
|
const text = await response.text();
|
|
signal?.throwIfAborted();
|
|
if (Buffer.byteLength(text) > maxBytes) throw tooLarge();
|
|
return text;
|
|
}
|
|
const cancel = () => {
|
|
void reader.cancel().catch(() => {});
|
|
};
|
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
const decoder = new TextDecoder();
|
|
let out = '';
|
|
let total = 0;
|
|
try {
|
|
for (;;) {
|
|
signal?.throwIfAborted();
|
|
const { done, value } = await reader.read();
|
|
signal?.throwIfAborted();
|
|
if (done) break;
|
|
total += value.byteLength;
|
|
if (total > maxBytes) throw tooLarge();
|
|
out += decoder.decode(value, { stream: true });
|
|
}
|
|
return out + decoder.decode();
|
|
} catch (error) {
|
|
cancel();
|
|
throw error;
|
|
} finally {
|
|
signal?.removeEventListener('abort', cancel);
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
|
|
/** Parse a fetch() JSON response only after enforcing a hard byte cap. */
|
|
export async function readResponseJsonCapped(response, maxBytes, signal) {
|
|
return JSON.parse(await readResponseTextCapped(response, maxBytes, signal));
|
|
}
|
|
|
|
/**
|
|
* Return the existing promise for a cache key, or create one and remove it
|
|
* only when that exact promise settles.
|
|
*/
|
|
export function coalesceProxyRequest(inFlight, key, create) {
|
|
const existing = inFlight.get(key);
|
|
if (existing) return { promise: existing, shared: true };
|
|
let promise;
|
|
promise = Promise.resolve()
|
|
.then(create)
|
|
.finally(() => {
|
|
if (inFlight.get(key) === promise) inFlight.delete(key);
|
|
});
|
|
inFlight.set(key, promise);
|
|
return { promise, shared: false };
|
|
}
|
|
|
|
/**
|
|
* Read a fetch Response body as text while enforcing a hard byte cap during
|
|
* the read — so a malicious or buggy upstream that streams an unbounded body
|
|
* (no/oversized Content-Length, chunked) can't OOM the proxy. Returns
|
|
* { tooLarge, text }. Cancels the stream as soon as the cap is crossed.
|
|
* @param {Response} upstream - fetch() response.
|
|
* @param {number} maxBytes - hard ceiling on decoded bytes.
|
|
* @returns {Promise<{tooLarge: boolean, text: string}>}
|
|
*/
|
|
export async function readCappedResponseText(upstream, maxBytes) {
|
|
const declared = Number(upstream.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
try {
|
|
await upstream.body?.cancel();
|
|
} catch {
|
|
/* no-op */
|
|
}
|
|
return { tooLarge: true, text: '' };
|
|
}
|
|
if (
|
|
!upstream.body ||
|
|
typeof upstream.body[Symbol.asyncIterator] !== 'function'
|
|
) {
|
|
const text = await upstream.text();
|
|
return text.length > maxBytes
|
|
? { tooLarge: true, text: '' }
|
|
: { tooLarge: false, text };
|
|
}
|
|
const decoder = new TextDecoder();
|
|
let text = '';
|
|
let total = 0;
|
|
for await (const chunk of upstream.body) {
|
|
total += chunk.length;
|
|
if (total > maxBytes) {
|
|
try {
|
|
await upstream.body.cancel();
|
|
} catch {
|
|
/* no-op */
|
|
}
|
|
return { tooLarge: true, text: '' };
|
|
}
|
|
text += decoder.decode(chunk, { stream: true });
|
|
}
|
|
text += decoder.decode();
|
|
return { tooLarge: false, text };
|
|
}
|