Skip to content

fix(express)!: require Express 5 and add router error middleware#89

Merged
Bccorb merged 1 commit into
mainfrom
fix/require-express-5
Jul 20, 2026
Merged

fix(express)!: require Express 5 and add router error middleware#89
Bccorb merged 1 commit into
mainfrom
fix/require-express-5

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes #73 (the last remaining item, item 4). The other four items landed in #88.

Requires Express 5 rather than adding an async wrapper to keep Express 4 working. Taking the break now while the package is pre-1.0, so adopters are not asked to absorb it later at a higher cost.

Why the peer range and not a wrapper

Every route handler already returns its promise to Express. Registrations use concise-body arrows ((req, res) => handler(...)), and the single block-body arrow is async, so nothing is actually discarded. Express 5 forwards those rejections to error middleware; Express 4 does not.

Reproduced with the same code and input (upstream fetch rejects), varying only the Express version:

Express Result
4.22.2 no response, client aborts (ECONNABORTED) after 2s
5.2.1 500 immediately

So the hang was purely an Express 4 compatibility issue, and moving the floor to 5 resolves it without new runtime code.

Migration cost

Scanned for Express 4 to 5 breakage in this package and found none: no wildcard, optional, or regex route paths (path-to-regexp v8 is the usual source of breakage), no req.param(), res.send(status), res.json(obj, status), app.del, or redirect('back'). res.clearCookie passes only domain and path. buildProxyQueryString handles only strings and string arrays, so the simple query parser default is a no-op here.

CI already ran entirely on Express 5, so this package needed no changes to support it.

Error middleware

Requiring Express 5 stops the hang but hands the request to Express's built-in error handler, which answers with an HTML stack trace including absolute server paths whenever NODE_ENV is not production. That is a poor default for an auth package and also breaks the JSON contract every other route follows.

The router now registers its own error middleware: 500 with a JSON { "error": "internal_error" } body, cause logged server side, and it delegates to next(err) when headers were already sent.

Tests

Added packages/express/tests/routeErrorHandling.test.js (3 tests), mirroring the existing suites.

Verified they are not vacuous by removing the middleware and re-running. The "answers 500" test passes either way, since Express 5 alone returns 500. The JSON-body/no-leak and server-side-logging tests both fail without it, which is the correct split.

Verification

pnpm build clean. pnpm test at the workspace root: core 15 suites / 60 tests, express 20 suites / 76 tests, all passing.

One transient failure in tests/cookieSecurity.test.js appeared in a single run on a loaded machine and did not reproduce. It passes in isolation, passes in the full suite across 6 subsequent clean runs, and --detectOpenHandles on the new test file is clean. Noting it rather than omitting it. That file mutates process.env.NODE_ENV, which is worth keeping an eye on, though it is pre-existing and unrelated to this change.

Changeset is a minor bump, which is the pre-1.0 convention for a breaking change.

Under Express 4 a rejected handler promise was never routed anywhere, so
an upstream failure left the request hanging until the client timed out.
Express 5 forwards rejected handler promises to error middleware, which
terminates those requests properly.

The Express built-in error handler answers with an HTML stack trace
including absolute server paths whenever NODE_ENV is not production, so
the router now registers its own error middleware that returns a JSON
500 and logs the cause server side.

BREAKING CHANGE: express and @types/express peer ranges are now >=5.0.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: minor correctness bugs (204 body, empty-body throw, grouped metrics query, async errors)

1 participant