Quality gates / quality (pull_request) Canceled after 0s
Brings #82 up to today's `main` (1bbcc10) and resolves the two conflicts as ordered
unions: `package.json` `test`/`check:syntax` and the release builder's syntax allow-list
and evidence keys. Both sides only appended, so nothing was dropped.
Verifying the merge against main's RC attack suite (#40) found two real defects, each
fixed test-first:
- An oversized body destroyed the socket mid-upload, so the caller saw `fetch failed`
instead of the 413 it had already earned, and `security_attack_suite.mjs` could not
complete at all. New test in `tests/analyze-ingress.acceptance.test.js` failed with
`fetch failed` before the fix; `src/http-body.js` now keeps draining without buffering
and refuses on `end`, so the ceiling still holds and the refusal is readable.
- #16's per-caller budget turned the suite's own load phase into 8/8 429s, which would
have published refusals as analysis latency. The load phase now runs against a second
permissive target and the suite refuses to publish a load figure if any sample was
refused. `buildReport` also publishes `evidenceVerified`, so the verdict is
re-derivable from the report — asserted by #40's acceptance test, which only trips once
the verdict is GO.
Evidence on the merged tree: `npm test` 277/277, `test:security-attack` 8/8, the RC attack
suite returns VERDICT: GO with both P0 ingress cases passing (main is BLOCKED on exactly
those two), 14 browser acceptance suites green, and the release demo records end to end
(8:13, 97 captions) including the on-camera refusal scene.
80 lines
2.5 KiB
JavaScript
80 lines
2.5 KiB
JavaScript
// Bounded request-body reading for the API boundary (issue #16).
|
|
//
|
|
// Three limits have to fail before any provider work starts: the byte ceiling, a deadline for a body
|
|
// that never arrives, and JSON validity. All three refuse with a client-visible status and a fixed
|
|
// message; none of them logs or returns the body.
|
|
|
|
const COPY = Object.freeze({
|
|
too_large: [413, 'Request is too large.'],
|
|
invalid_json: [400, 'Invalid JSON request.'],
|
|
body_timeout: [408, 'The request took too long to arrive. Try again.'],
|
|
});
|
|
|
|
export class BodyError extends Error {
|
|
constructor(code) {
|
|
const [status, message] = COPY[code] || COPY.invalid_json;
|
|
super(message);
|
|
this.name = 'BodyError';
|
|
this.code = code;
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export function readJsonBody(req, { maxBytes = 6 * 1024 * 1024, timeoutMs = 15_000 } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
let tooLarge = false;
|
|
let size = 0;
|
|
const chunks = [];
|
|
const finish = callback => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(deadline);
|
|
req.removeListener('data', onData);
|
|
req.removeListener('end', onEnd);
|
|
req.removeListener('error', onError);
|
|
callback();
|
|
};
|
|
const deadline = setTimeout(() => {
|
|
finish(() => {
|
|
// Stop reading and let the caller answer 408; the server destroys the socket once the
|
|
// refusal is written, so a half-read body cannot be held open indefinitely.
|
|
req.pause?.();
|
|
reject(new BodyError('body_timeout'));
|
|
});
|
|
}, timeoutMs);
|
|
|
|
function onData(chunk) {
|
|
if (settled) return;
|
|
// Past the ceiling the body is already refused: keep draining it without buffering so the
|
|
// caller can still read the 413. Destroying the socket here would surface as "fetch failed"
|
|
// and leave the caller with no status to act on.
|
|
if (tooLarge) return;
|
|
size += chunk.length;
|
|
if (size > maxBytes) {
|
|
tooLarge = true;
|
|
chunks.length = 0;
|
|
return;
|
|
}
|
|
chunks.push(chunk);
|
|
}
|
|
function onEnd() {
|
|
finish(() => {
|
|
if (tooLarge) return reject(new BodyError('too_large'));
|
|
try {
|
|
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
} catch {
|
|
reject(new BodyError('invalid_json'));
|
|
}
|
|
});
|
|
}
|
|
function onError(error) {
|
|
finish(() => reject(error));
|
|
}
|
|
|
|
req.on('data', onData);
|
|
req.on('end', onEnd);
|
|
req.on('error', onError);
|
|
});
|
|
}
|