Skip to content

Commit eb49bfa

Browse files
committed
feat(router): isolate warning to Express routers and support array paths
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.
1 parent 4358fd4 commit eb49bfa

5 files changed

Lines changed: 122 additions & 8 deletions

File tree

‎lib/application.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var methods = require('./utils').methods;
2121
var compileETag = require('./utils').compileETag;
2222
var compileQueryParser = require('./utils').compileQueryParser;
2323
var compileTrust = require('./utils').compileTrust;
24+
var pathsOverlap = require('./utils').pathsOverlap;
2425
var resolve = require('node:path').resolve;
2526
var once = require('once')
2627
var Router = require('router');
@@ -477,7 +478,7 @@ methods.forEach(function (method) {
477478

478479
if (method === 'head' && this.router && Array.isArray(this.router.stack)) {
479480
var hasPrecedingGet = this.router.stack.some(function (layer) {
480-
return layer.route && layer.route.path === path && layer.route.methods && layer.route.methods.get;
481+
return layer.route && pathsOverlap(layer.route.path, path) && layer.route.methods && layer.route.methods.get;
481482
});
482483
if (hasPrecedingGet) {
483484
process.emitWarning(

‎lib/express.js‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,24 @@ exports.application = proto;
6363
exports.request = req;
6464
exports.response = res;
6565

66+
var utils = require('./utils');
67+
6668
/**
67-
* Wrap Router.prototype.head to warn if declared after a GET route on the same path.
69+
* Express router factory wrapping Router to detect and warn on shadowed HEAD routes.
70+
*
71+
* @param {Object} [options]
72+
* @return {Function} router
73+
* @public
6874
*/
6975

70-
if (Router.prototype && typeof Router.prototype.head === 'function') {
71-
var origRouterHead = Router.prototype.head;
72-
Router.prototype.head = function head(path) {
76+
function expressRouter(options) {
77+
var router = new Router(options);
78+
var origHead = router.head;
79+
80+
router.head = function head(path) {
7381
if (Array.isArray(this.stack)) {
7482
var hasPrecedingGet = this.stack.some(function (layer) {
75-
return layer.route && layer.route.path === path && layer.route.methods && layer.route.methods.get;
83+
return layer.route && utils.pathsOverlap(layer.route.path, path) && layer.route.methods && layer.route.methods.get;
7684
});
7785
if (hasPrecedingGet) {
7886
process.emitWarning(
@@ -81,16 +89,21 @@ if (Router.prototype && typeof Router.prototype.head === 'function') {
8189
);
8290
}
8391
}
84-
return origRouterHead.apply(this, arguments);
92+
return origHead.apply(this, arguments);
8593
};
94+
95+
return router;
8696
}
8797

98+
expressRouter.Route = Router.Route;
99+
expressRouter.prototype = Router.prototype;
100+
88101
/**
89102
* Expose constructors.
90103
*/
91104

92105
exports.Route = Router.Route;
93-
exports.Router = Router;
106+
exports.Router = expressRouter;
94107

95108
/**
96109
* Expose middleware

‎lib/utils.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,27 @@ function parseExtendedQueryString(str) {
269269
allowPrototypes: true
270270
});
271271
}
272+
273+
/**
274+
* Check if two route paths overlap (supporting string and array paths).
275+
*
276+
* @param {String|Array} existingPath
277+
* @param {String|Array} newPath
278+
* @return {Boolean}
279+
* @api private
280+
*/
281+
282+
exports.pathsOverlap = function pathsOverlap(existingPath, newPath) {
283+
if (Array.isArray(existingPath) && Array.isArray(newPath)) {
284+
return existingPath.some(function (p) {
285+
return newPath.indexOf(p) !== -1;
286+
});
287+
}
288+
if (Array.isArray(existingPath)) {
289+
return existingPath.indexOf(newPath) !== -1;
290+
}
291+
if (Array.isArray(newPath)) {
292+
return newPath.indexOf(existingPath) !== -1;
293+
}
294+
return existingPath === newPath;
295+
};

‎test/Router.js‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,5 +686,54 @@ describe('Router', function () {
686686
done()
687687
})
688688
})
689+
690+
it('should emit a warning when GET is declared with an array containing the HEAD path', function (done) {
691+
var router = new Router()
692+
var warnings = []
693+
694+
function onWarning(warning) {
695+
warnings.push(warning)
696+
}
697+
698+
process.on('warning', onWarning)
699+
700+
router.get(['/a', '/b'], function (req, res) {
701+
res.end('get')
702+
})
703+
704+
router.head('/a', function (req, res) {
705+
res.end()
706+
})
707+
708+
process.nextTick(function () {
709+
process.removeListener('warning', onWarning)
710+
assert.strictEqual(warnings.length, 1)
711+
assert.strictEqual(warnings[0].name, 'ExpressWarning')
712+
assert.ok(warnings[0].message.includes('HEAD route for "/a" declared after GET route will be shadowed'))
713+
done()
714+
})
715+
})
716+
717+
it('should not mutate the underlying router package prototype', function () {
718+
var StandaloneRouter = require('router')
719+
var standalone = new StandaloneRouter()
720+
721+
var warnings = []
722+
function onWarning(warning) {
723+
warnings.push(warning)
724+
}
725+
726+
process.on('warning', onWarning)
727+
728+
standalone.get('/standalone', function (req, res) {
729+
res.end()
730+
})
731+
standalone.head('/standalone', function (req, res) {
732+
res.end()
733+
})
734+
735+
process.removeListener('warning', onWarning)
736+
assert.strictEqual(warnings.length, 0)
737+
})
689738
})
690739
})

‎test/app.head.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,31 @@ describe('app.head()', function(){
140140
done()
141141
})
142142
})
143+
144+
it('should emit a warning when GET is declared with an array containing the HEAD path', function (done) {
145+
var app = express()
146+
var warnings = []
147+
148+
function onWarning(warning) {
149+
warnings.push(warning)
150+
}
151+
152+
process.on('warning', onWarning)
153+
154+
app.get(['/a', '/b'], function (req, res) {
155+
res.send('get')
156+
})
157+
158+
app.head('/a', function (req, res) {
159+
res.end()
160+
})
161+
162+
process.nextTick(function () {
163+
process.removeListener('warning', onWarning)
164+
assert.strictEqual(warnings.length, 1)
165+
assert.strictEqual(warnings[0].name, 'ExpressWarning')
166+
assert.ok(warnings[0].message.includes('HEAD route for "/a" declared after GET route will be shadowed'))
167+
done()
168+
})
169+
})
143170
})

0 commit comments

Comments
 (0)