Skip to content

fix: restore req/res prototypes when a mounted app hands control back - #7428

Open
lazerg wants to merge 4 commits into
expressjs:masterfrom
lazerg:fix/issue-7427-mounted-app-prototype
Open

fix: restore req/res prototypes when a mounted app hands control back#7428
lazerg wants to merge 4 commits into
expressjs:masterfrom
lazerg:fix/issue-7427-mounted-app-prototype

Conversation

@lazerg

@lazerg lazerg commented Aug 25, 2026

Copy link
Copy Markdown

app.handle swaps the req/res prototypes for its own, but only app.use() puts them back, inside the mounted_app closure it wraps around a sub-app. Mount an app with router.use() instead and the swap is never undone, so every middleware that runs after the sub-app calls next() reads req.app, req.ip, req.secure and req.hostname through the sub-app's settings rather than the parent's.

app.handle now saves the prototypes it replaces and restores them before invoking the callback it was handed, so the cleanup happens on whichever path the app was mounted through. The issue suggested patching restore() inside router, but the swap originates here, and doing it here also covers any other caller that passes a callback without needing a router release.

This covers the prototype half of #7427 only. A router-mounted app still gets no mount event and so does not inherit trust proxy, which would need router to know about express apps.

Fixes #7427

@santusht06

Copy link
Copy Markdown

Great work on identifying the core prototype swap in app.handle!

The approach of capturing origReqProto / origResProto when callback is provided and restoring them inside done is definitely the clean and canonical way to address #7427 across all mounting paths.

To make the fix completely airtight for maintainers and prevent future regressions, there are a few important edge cases that should also be covered in the test suite:

  1. Error Handling Pipeline (next(err)): When a sub-app encounters an error and triggers next(new Error(...)), verifying that the parent application's 4-argument error middleware (app.use((err, req, res, next) => ...)) also receives the restored prototype and req.app === parentApp.
  2. Custom Prototype Extensions: Verifying that custom helper methods attached to the parent application's prototype (e.g. app.request.customHelper()) remain callable and intact on req and res after delegating back.
  3. Context Restoration: Explicitly asserting req.app === parentApp in addition to Object.getPrototypeOf(req) === app.request.

I've put together a PR in #7434 with these comprehensive test cases if you'd like to incorporate them or collaborate to help get this merged quickly!

@lazerg

lazerg commented Aug 27, 2026

Copy link
Copy Markdown
Author

Added a test for the next(err) path (e418c6f): the parent's error middleware also gets the restored prototypes. The other two cases don't need separate assertions — req.app is defined on the same request object the prototype check already compares, and any custom methods on app.request/app.response come along with that same identity check.

cultosagent added a commit to cultosagent/dogma-registry that referenced this pull request Aug 31, 2026

@kilisamemarisaaa kilisamemarisaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reproduced a compatibility regression in the callback wrapper added in lib/application.js.

The wrapper restores req and res to the prototypes that existed before app.handle. When an Express app is passed directly to a bare express.Router() via router.use(child), those incoming prototypes are Node's IncomingMessage/ServerResponse, not a parent Express application's prototypes. The downstream router middleware therefore loses the child's Express API entirely.

Reproduction (run from the PR head e418c6f0f4aa4e3a3f93a4ff61e379511e8ece05):

const express = require('./')
const request = require('supertest')
const child = express()
const router = express.Router()
child.use((req, res, next) => next())
router.use(child)
router.use((req, res) => res.end(JSON.stringify({
  childProto: Object.getPrototypeOf(req) === child.request,
  app: req.app === child,
  hasIp: typeof req.ip === 'string',
  resProto: Object.getPrototypeOf(res) === child.response
})))
const app = (req, res) => router.handle(req, res, err => {
  if (err) { res.statusCode = 500; res.end(err.message) }
})
request(app).get('/').end((err, res) => {
  if (err) throw err
  console.log(res.status, res.text)
})

PR head output: 200 {"childProto":false,"app":false,"hasIp":false,"resProto":false}. The same script on base 023767fe9872e029271df1418f73401bff20ff40 outputs all four values as true.

This is a regression beyond the reported Express-parent case: downstream middleware mounted on a bare router no longer has Express methods such as req.ip or res.json. The discussion on #7427 also states that router.use() treats an app as middleware and that middleware request/response modifications should persist.

Please narrow the restoration to a boundary that knows it is returning to an Express parent (or move the cleanup into the appropriate mounting layer), and add a bare-router regression test before merging.

@lazerg

lazerg commented Sep 4, 2026

Copy link
Copy Markdown
Author

Confirmed, thanks. Reproduced the exact output on e418c6f.

app.handle now only restores prototypes when the "before" prototype already carries an .app (an own property that only exists on app.request/app.response). That's true when a real Express app already ran app.handle before this one, so restoring returns control to that Express parent's prototypes. It's false for a bare router, since no Express app.handle set that prototype, so nothing gets restored and the child's prototypes persist for downstream middleware.

Fixed in 5f801a1, plus a regression test in test/app.use.js covering the bare-router case. Re-ran both prior mount-restore tests too, they still pass.

@kilisamemarisaaa kilisamemarisaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the latest head 5f801a1 after the bare-router guard. On Windows/Node v24.12.0, the three prototype tests in test/app.use.js pass (3/3), and npm run lint passes. The full suite reaches 1262 passing with one unrelated Windows-only failure in the untouched express.static redirect-encoding test (test/express.static.js:510). The new reqProto.app check restores Express-parent prototypes while leaving child prototypes intact for a bare router, matching the intended mounting semantics. I found no further issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants