Repository navigation
Regression tests can miss res.status range-validation failures #7503
Description
Activity
Independently confirmed on
7ef9844(which is still the current main tip) — and I can pin down why the HTTP tests can't catch this mutation, which matters for what the fix must assert.The mutation (replacing
code < 100 || code > 999withfalse) removes the only fail-fast point:lib/response.js:71-73throwsRangeError("Invalid status code: ... . Status code must be greater than 99 and less than 1000.")synchronously at thestatus()call. After the mutation,res.status(99).end()flows through untouched and only dies later, when Node'swriteHeadraisesERR_HTTP_INVALID_STATUS_CODE("Invalid status code: 99").That error is caught by Express's error handler and served as a 500 whose body matches the existing
/Invalid status code/expectation intest/res.status.js— the same regex that matches Express's ownRangeErrormessage. So the masking is message collision, not timing: no HTTP-level assertion, however carefully awaited, can distinguish "Express rejected this synchronously" from "Node rejected this at writeHead". Only asserting the throw at thestatus()call site pins the contract, which is exactly what the proposed direct assertions do.One scope-precision note: the masking affects only the out-of-range integer cases (99, 1000). The non-integer family (200.1, NaN, strings, null, undefined) still throws
TypeErrorfrom the integer check atlib/response.js:67, so those HTTP tests would keep passing for the right reason even with the range check mutated away. Worth stating in the test names/docs so a future regression in only the range check can't hide behind the still-green non-integer tests.Local verification on
7ef9844: with the range check mutated,Object.create(express.response).status(99)and.status(1000)no longer throw (RangeError expected), whilemocha test/res.status.jsstill passes 16/16 — confirming the report's reproduction end to end.Thanks for independently reproducing this on
7ef9844, and for the clear write-up of the mechanism, agreed that it's message collision rather than timing: Node'sERR_HTTP_INVALID_STATUS_CODEmatches the same/Invalid status code/expectation, so only a direct assertion at thestatus()call can pin Express's own range check.Also agreed on the scope: the non-integer cases still fail fast via the
Number.isIntegerTypeError, so only 99 and 1000 are affected. #7504 adds directRangeErrorassertions for exactly those two, plus direct acceptance checks for 100 and 999. Happy to make the test names explicit about the range check if maintainers prefer.
The existing res.status implementation correctly accepts the inclusive 100-999 range and rejects out-of-range integers. The regression tests can observe Node's later HTTP validation instead of Express's own synchronous validation.
At 7ef9844, temporarily replacing the range condition in lib/response.js with if (false) leaves the existing HTTP checks for 99 and 1000 passing: Node rejects those values when the response ends, with an error matching the existing expectation.
The test-only correction is to assert directly that Object.create(express.response).status(99) and status(1000) raise RangeError, while keeping the HTTP checks. Direct tests also cover acceptance of 100 and 999, the returned response object, and the stored statusCode. Testing 100 directly avoids interim-response behavior over HTTP.
Reproduction:
Prepared with AI assistance using Supertest, a test-effectiveness audit skill. Its concrete benefit here was revealing a validation regression hidden by the HTTP boundary and verifying that the strengthened tests detect it. This report concerns test coverage; application behavior is unchanged.
Environment: Express 5.2.1, Node 24.13.0, Linux.