When an express() sub-app is mounted via router.use() instead of app.use(), the request and response prototypes are swapped by app.handle but never restored. The sub-app also never fires the mount event, so it does not inherit the parent's trust proxy setting. Any middleware that runs after the sub-app calls next() reads req.ip, req.secure, and req.hostname under the wrong app's trust policy, silently and without any error.
app.handle (lib/application.js:169-170) performs:
js
Object.setPrototypeOf(req, this.request)
Object.setPrototypeOf(res, this.response)
The only code that reverses this swap is the mounted_app closure built by app.use at lib/application.js:230-237. router.use never builds that closure, so the swap is permanent for the lifetime of the request.
The router already saves and restores request state across boundaries at index.js:171:
js
let done = restore(callback, req, 'baseUrl', 'next', 'params')
The prototype is simply not on the list.
Add proto (or Object.getPrototypeOf equivalents) to the restore call so the router's own cleanup undoes the prototype swap on every exit path — next(), next(err), and unhandled throws alike.
js
// index.js — inside Router.prototype.handle, before the loop
var reqProto = Object.getPrototypeOf(req)
var resProto = Object.getPrototypeOf(res)
var done = restore(callback, req, 'baseUrl', 'next', 'params')
var _done = done
done = function(err) {
Object.setPrototypeOf(req, reqProto)
Object.setPrototypeOf(res, resProto)
_done(err)
}
When an express() sub-app is mounted via router.use() instead of app.use(), the request and response prototypes are swapped by app.handle but never restored. The sub-app also never fires the mount event, so it does not inherit the parent's trust proxy setting. Any middleware that runs after the sub-app calls next() reads req.ip, req.secure, and req.hostname under the wrong app's trust policy, silently and without any error.
app.handle (lib/application.js:169-170) performs:
js
Object.setPrototypeOf(req, this.request)
Object.setPrototypeOf(res, this.response)
The only code that reverses this swap is the mounted_app closure built by app.use at lib/application.js:230-237. router.use never builds that closure, so the swap is permanent for the lifetime of the request.
The router already saves and restores request state across boundaries at index.js:171:
js
let done = restore(callback, req, 'baseUrl', 'next', 'params')
The prototype is simply not on the list.
Add proto (or Object.getPrototypeOf equivalents) to the restore call so the router's own cleanup undoes the prototype swap on every exit path — next(), next(err), and unhandled throws alike.
js
// index.js — inside Router.prototype.handle, before the loop
var reqProto = Object.getPrototypeOf(req)
var resProto = Object.getPrototypeOf(res)
var done = restore(callback, req, 'baseUrl', 'next', 'params')
var _done = done
done = function(err) {
Object.setPrototypeOf(req, reqProto)
Object.setPrototypeOf(res, resProto)
_done(err)
}