fix: normalize all-slash mount paths in app.use to '/'#7286
fix: normalize all-slash mount paths in app.use to '/'#7286youcefzemmar wants to merge 1 commit into
Conversation
Mounting with `app.use('//', subApp)` previously slipped past the
router's `/`-fast-path because `loosen('//')` strips trailing slashes
to `''`, leaving an empty pattern that `path-to-regexp` happily matches
against every request. The router then treats the layer as "any path"
without setting the `slash` fast-path flag — a behavior that diverges
from `app.use('/', subApp)` and surprised the reporter in expressjs#4557.
Collapse any all-slash string path (`'/'`, `'//'`, `'///'`, …) to `'/'`
in `app.use` before handing it to the router. Inner repeated slashes
(`'/foo//bar'`) are intentionally left alone — that is a broader
behavior change outside the scope of this issue.
Fixes expressjs#4557.
notadev99
left a comment
There was a problem hiding this comment.
Really thorough writeup — the trace through loosen() and the "surface workaround vs. fixing it in router" framing are exactly right, and the guard is correctly scoped (/^\/+$/ leaves /foo//bar alone). A few things:
Does the new test fail on main? Before the fix, '//' matched every request and routed into sub. Since sub only has / and /secret, it looks like /->sub-root, /secret->sub-secret, and /missing->404 would all pass even without the guard — meaning the test pins the new behavior but may not catch the regression. Is there a request whose status or body differs before vs. after? Asserting on that is what makes it a true regression test.
Array mount paths. app.use(['//'], sub) skips the typeof path === 'string' guard, so the all-slash exposure still exists for the array form. Intentionally out of scope, or should normalization run per element?
Resulting semantics. Normalizing '//' to '/' makes the sub-app reachable at root, whereas #4557's reporter expected those routes to be unreachable. This makes the behavior predictable rather than hidden, which seems right — just worth confirming root-mount is the intended semantics over erroring on an all-slash path.
For
'//',loosenstrips both slashes to''. The layer'sslashflag isfalse(becausepath !== '/'), so matching falls through topathRegexp.match('', { end: false, trailing: true }). That matcher matches every request — the layer behaves like a wildcard prefix, just without the fast-path metadata. Mounting at'///'and longer reduces to the same case.In the issue, the reporter mounted dev/test routes under
'//'expecting them to be unreachable from/, but the layer matched anyway and exposed those routes.Fix
One guard, right after
app.useresolves the path arg:The fix stays scoped to all-slash strings. Inner repeated slashes like
'/foo//bar'are deliberately left alone — collapsing them is a broader behavior change, the same one that took PR #7054 out of scope.Testing
npm test— 1250 passing, including the newshould treat all-slash paths as a root mountcase intest/app.use.jsthat exercises'//'and'///'end-to-end.npm run lint— clean.The new test locks in three things:
/and/secretreach the sub-app, an unrelated path like/missingstill 404s (i.e. the layer is not a global wildcard), and'///'normalizes the same way as'//'.Notes
Root cause technically lives in
router'sloosen()(treating'//'as''). Fixing it in express is a surface workaround — ifrouterever normalizes this itself, the guard here is harmless but redundant.Fixes #4557.