Skip to content

Fix error thrown in a mounted router routing to a pre-mount error handler (#239) - #353

Open
emilioastarita wants to merge 1 commit into
dimdenGD:mainfrom
emilioastarita:fix/optimized-router-error-handler-order
Open

Fix error thrown in a mounted router routing to a pre-mount error handler (#239)#353
emilioastarita wants to merge 1 commit into
dimdenGD:mainfrom
emilioastarita:fix/optimized-router-error-handler-order

Conversation

@emilioastarita

@emilioastarita emilioastarita commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #239.

Problem

An error thrown inside a router mounted with app.use(path, router) should be caught by an error handler declared after the mount (as express does), not one declared before it. ultimate-express routed it to the handler declared before the mount:

// router.js
const router = require("ultimate-express").Router();
router.post("/path", (req, res) => { throw new Error("Whoops"); });
module.exports = router;

// index.js
const app = express();
app.use(function jsonErrorHandler(err, req, res, next) { if (err) return res.send("JSON error"); next(); });
app.use("/route", require("./router"));
app.use(function errorHandler(err, req, res, next) { if (err) return res.json(err.toString()); next(); });
app.listen(3000);
// POST /route/path -> express: "Error: Whoops"  |  ultimate-express: "JSON error"

The reporters noted it was flaky and only happened when the router was imported from another module. Both observations follow from the cause below; verified against express 4 and express 5.2.x.

Cause

The route optimizer replays a mounted router's leaf route inline in the parent's uWS fast-path chain. Normal dispatch attributes an error propagating out of a sub-router to the mount (req._errorKey = <mount route key> after the sub-router returns), but the optimized chain never did this, so req._errorKey kept the leaf route's key.

A router's routes get a lower routeKey than the parent's own middlewares when the router is constructed before them — which is exactly what happens when it is required from another module. An error handler is selected when route.routeKey >= req._errorKey, so a pre-mount handler (key higher than the leaf's) wrongly qualified. "Flaky / only from another module" is this ordering; "only sometimes" was the old ~100ms optimizer window (now compiled at listen, so it is consistent).

Fix

When the optimized chain is exhausted and dispatch falls back to normal routing, attribute a pending error to the mount's routeKey (mirroring normal dispatch), and make skipUntil the mount boundary rather than the router's leaf so fallthrough resumes after the mount instead of re-running the parent middlewares that precede it. Both are gated on the chain actually crossing a mounted router (the keepMount marker), so plain top-level optimized routes are unaffected.

Test

New tests/tests/routers/mounted-router-error-handler-order.js: a router defined before the parent's handlers (reproducing the lower-routeKey condition in a single file), with an error-throwing route and a normal route. Fails on main, passes with the fix, output identical to express 4 and express 5. Full suite passes.

…dler

An error thrown inside a router mounted with app.use(path, router) must be
caught by an error handler declared AFTER the mount, like express does, not
one declared before it. When the router's routes were compiled onto the uWS
fast path, an error thrown in the router was instead caught by an error
handler declared before the mount.

The optimized chain replays the router's leaf route inline in the parent, so
the mount-boundary error attribution that normal dispatch performs
(req._errorKey = <mount route key> after a sub-router returns an error) never
happened; req._errorKey kept the leaf route's key. A router's routes can get
a lower routeKey than the parent's own middlewares -- e.g. when the router is
required from another module and therefore constructed before them -- so a
pre-mount error handler (routeKey >= the leaf's key) would wrongly qualify.
This is why the bug only showed up for routers imported from another module,
and only once the optimizer had compiled the route.

When the optimized chain is exhausted and dispatch falls back to normal
routing, attribute a pending error to the mount's routeKey (mirroring normal
dispatch), and make skipUntil the mount boundary rather than the router's leaf
so fallthrough resumes after the mount in the parent instead of re-running the
parent middlewares that precede it.

Fixes dimdenGD#239.
@emilioastarita
emilioastarita force-pushed the fix/optimized-router-error-handler-order branch from 885cdee to 4bb682a Compare July 24, 2026 13:00
@emilioastarita
emilioastarita marked this pull request as ready for review July 24, 2026 13:10
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.

Incorrect sequence of middlewares when importing a router from another module

1 participant