Skip to content

Fix res.render using the outer app's views for mounted sub-apps - #352

Merged
dimdenGD merged 1 commit into
dimdenGD:mainfrom
emilioastarita:fix/render-subapp-views
Jul 23, 2026
Merged

Fix res.render using the outer app's views for mounted sub-apps#352
dimdenGD merged 1 commit into
dimdenGD:mainfrom
emilioastarita:fix/render-subapp-views

Conversation

@emilioastarita

Copy link
Copy Markdown
Contributor

Problem

A mounted sub-app's res.render should resolve views with the sub-app's own views / view engine / engine() settings — express does this by looking up this.req.app in res.render, and req.app points at the sub-app during its dispatch. ultimate-express instead calls this.app.render(...), and res.app is fixed at response construction to the outer app, so a mounted sub-app's view configuration was ignored and rendering failed with Failed to lookup view ... in views directory <outer app's ./views> (or No default engine was specified when the outer app has no view config).

Repro

const express = require("ultimate-express");
const outer = express();
const sub = express();
sub.set("views", "/dir/with/index.ejs");
sub.set("view engine", "ejs");
sub.engine("ejs", require("ejs").renderFile);
sub.get("/page", (req, res) => res.render("index"));
outer.use("/sub", sub);
outer.listen(3000);
// GET /sub/page -> express renders; ultimate-express throws the lookup error

Verified side by side against express 4 and express 5.2.x.

Fix

  1. res.render now delegates to this.req.app.render(...), matching express.
  2. The route optimizer's compiled chain (_compileOptimizedRoutes) now records the mounted Application on the compiled mount route (mountApp) and _routeRequest swaps req.app to it during dispatch — normal dispatch already swaps req.app when it enters a mounted Application, but the compiled mount route has no callback to do so.
  3. On fallthrough from an exhausted optimized path back to normal routing, req.app is restored to the top-level app, so a sub-app route that calls next() into an outer handler doesn't leak the sub-app's config.

This is rebased on top of the refactor route optimization commit, and (2) is implemented in that new compiled-chain mechanism rather than the old setTimeout-based path.

Tests

New tests/tests/res/res-render-subapp.js (+ fixture tests/parts/subapp/index.ejs): a sub-app with its own views dir mounted on an outer app with a different views dir, rendering via the sub-app, plus a fallthrough case where the sub-app next()s into an outer catch-all that renders with the outer app's config. Output matches express 4 and express 5; full suite passes (the only failing test, app-cluster.js, fails identically on a clean checkout here — an unrelated process holds port 3000).

Context

Found integrating @bull-board/express's sub-app (which sets its own views/engine) behind an outer app on a production Express 5-style stack.

…ounted sub-app

Express resolves res.render() through this.req.app, which points at the
mounted sub-app during its dispatch, so a sub-app's 'views', 'view engine'
and engine() settings apply to its own routes. ultimate-express used
this.app, which is fixed at response construction to the outer app, so a
mounted sub-app's view configuration was ignored and rendering failed with
a view lookup error.

Three small pieces:
- res.render now delegates to this.req.app.render(), matching express.
- The optimized router path now swaps req.app to the mounted Application,
  mirroring what normal dispatch does, so req.app is consistent between
  the optimized and normal paths.
- When an optimized path is exhausted and dispatch falls back to normal
  routing, req.app is restored to the top-level app, so a sub-app route
  calling next() into an outer handler doesn't leak the sub-app.
@emilioastarita
emilioastarita marked this pull request as ready for review July 23, 2026 19:17
@dimdenGD
dimdenGD merged commit 5d677b5 into dimdenGD:main Jul 23, 2026
4 checks passed
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.

2 participants