#7427·express

router.use() silently accepts express() sub-apps without prototype restoration

Author: synapes620Created Aug 25, 2026Updated Sep 5, 2026
Labelsquestionrequire-triage

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) }