Skip to content

Regression tests can miss res.status range-validation failures #7503

Description

@renanmpimentel

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:

  1. Run npm run lint and npm run test-ci on the proposed tests: 1,263 tests pass.
  2. In an isolated copy, temporarily replace code < 100 || code > 999 in res.status with false.
  3. The two direct rejection assertions fail with Missing expected exception (RangeError). Without those direct assertions, the HTTP tests stay green.
  4. Restore the condition; all tests pass.

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.

Activity

  1. ToniAdreal commented on Oct 5, 2026

    @ToniAdreal

    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 > 999 with false) removes the only fail-fast point: lib/response.js:71-73 throws RangeError("Invalid status code: ... . Status code must be greater than 99 and less than 1000.") synchronously at the status() call. After the mutation, res.status(99).end() flows through untouched and only dies later, when Node's writeHead raises ERR_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 in test/res.status.js — the same regex that matches Express's own RangeError message. 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 the status() 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 TypeError from the integer check at lib/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), while mocha test/res.status.js still passes 16/16 — confirming the report's reproduction end to end.

  2. renanmpimentel commented on Oct 6, 2026

    @renanmpimentel
    Author

    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's ERR_HTTP_INVALID_STATUS_CODE matches the same /Invalid status code/ expectation, so only a direct assertion at the status() call can pin Express's own range check.

    Also agreed on the scope: the non-integer cases still fail fast via the Number.isInteger TypeError, so only 99 and 1000 are affected. #7504 adds direct RangeError assertions 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions