feat(app): warn when HEAD route is declared after GET route on the same path - #7435
feat(app): warn when HEAD route is declared after GET route on the same path#7435santusht06 wants to merge 3 commits into
Conversation
In Express, declaring a GET route automatically equips the route layer with an implicit HEAD fallback. When a developer declares a custom HEAD route after a GET route on the same path, the preceding GET route matches all HEAD requests, causing the custom HEAD handler to be silently shadowed and never called. This commit detects when a HEAD route is registered on a path with an existing GET route and emits a descriptive ExpressWarning to alert developers to declare HEAD routes before GET routes. Fixes expressjs#5004
kilisamemarisaaa
left a comment
There was a problem hiding this comment.
Could this also cover Router registrations? The runnable reproduction in #5004 uses express.Router(), but this check only runs in app[method], so Router#head never reaches it.
I reproduced this at 5e94a42b5238489cbdb5d1ee19d9ba655f6b128c with Express 5.2.1. The direct application sequence emits one warning, while the equivalent router sequence emits none and a HEAD request is still handled by the GET handler:
directAppWarnings: 1
routerWarnings: 0
routerHeadHandler: GET
The added app tests pass (6/6 for test/app.head.js) and lint is clean, but they do not exercise the linked issue's registration path. Since this PR does not change the router implementation, #5004's reproduction remains unchanged. Please add a regression test using express.Router() and cover that path (which may require a corresponding change in the router dependency), or narrow the PR scope rather than closing #5004.
Add Router.prototype.head wrapper to detect and emit an ExpressWarning when a HEAD route is registered on a path that already has a GET route in Router.stack. Also add regression tests in test/Router.js verifying warning emission on Router.
|
Thanks for the review and great catch, @kilisamemarisaaa! I have updated the PR in commit 4358fd4 to also wrap I also added regression tests in
All 1,261 tests pass cleanly across the full suite. |
kilisamemarisaaa
left a comment
There was a problem hiding this comment.
Thanks for addressing the Router path. I re-ran the original reproduction on 4358fd4f: it now emits one warning, and the full suite is 1,265/1,265 with lint passing.
I found two remaining cases before this fully covers the behavior:
- The wrapper in
lib/express.jsmutates the sharedrouterpackage prototype for the entire process. This minimal sequence shows the side effect:
const Router = require('router')
const originalHead = Router.prototype.head
require('express')
console.log(Router.prototype.head !== originalHead) // trueA standalone Router() created from that first import then emits an ExpressWarning when registering GET followed by HEAD, even though it is not an Express router. Could this be implemented in the router dependency itself (and consumed through a version update), or scoped to routers created through Express without patching the dependency's global prototype?
- The strict
layer.route.path === pathcomparison misses supported array paths:
router.get(['/a', '/b'], getHandler)
router.head('/a', headHandler)I sent HEAD /a on this head; getHandler handled it and zero warnings were emitted. An array-path regression test would keep the warning aligned with the actual shadowing behavior.
1. Scope Router.head warning check to expressRouter instances to prevent mutating the global Router.prototype from the standalone router package. 2. Add pathsOverlap utility to properly detect route shadowing when GET is declared with array paths (e.g. ['/a', '/b']). 3. Add regression tests for array route shadowing and prototype isolation.
|
Thanks again for the thorough review, @kilisamemarisaaa! I have addressed both items in commit eb49bfa:
All 1,264 tests across the entire suite pass cleanly with 0 lint errors. |
Problem
In Express, declaring an
app.get(path, ...)route automatically equips that route layer with an implicitHEADfallback.If a developer subsequently declares a custom
app.head(path, ...)route on the exact same path:When a client sends a
HEADrequest, the router evaluates layers in order of declaration. Because the precedingGETlayer matches allHEADrequests as a fallback, thehandleGethandler runs and the customhandleHeadroute is silently shadowed and never executed (Issue #5004).Solution
Inside
app[method]inlib/application.js, whenmethod === 'head', check if the router stack already contains an existing route for the samepathwithmethods.get = true.If detected, emit an
ExpressWarningusingprocess.emitWarningalerting the developer to declareHEADroutes beforeGETroutes to ensure they execute properly.Changes Made
lib/application.js: Added check and warning emission whenapp.head(path)is registered after an existingapp.get(path).test/app.head.js:HEADis declared afterGETon the same path.HEADis declared beforeGETon the same path.HEADandGETare declared on different paths.Test Plan
npm test(all 1,259 tests pass cleanly).npm run lint(passes with 0 errors).Fixes #5004