Fix error thrown in a mounted router routing to a pre-mount error handler (#239) - #353
Open
emilioastarita wants to merge 1 commit into
Open
Conversation
…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
force-pushed
the
fix/optimized-router-error-handler-order
branch
from
July 24, 2026 13:00
885cdee to
4bb682a
Compare
emilioastarita
marked this pull request as ready for review
July 24, 2026 13:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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, soreq._errorKeykept the leaf route's key.A router's routes get a lower
routeKeythan the parent's own middlewares when the router is constructed before them — which is exactly what happens when it isrequired from another module. An error handler is selected whenroute.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 atlisten, 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 makeskipUntilthe 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 (thekeepMountmarker), 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-routeKeycondition in a single file), with an error-throwing route and a normal route. Fails onmain, passes with the fix, output identical to express 4 and express 5. Full suite passes.