Skip to content

fix(pages): render masked basePath error routes#1441

Open
NathanDrake2406 wants to merge 11 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/basepath-error-pages
Open

fix(pages): render masked basePath error routes#1441
NathanDrake2406 wants to merge 11 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/basepath-error-pages

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented May 22, 2026

Overview

Field Details
Goal Match Next.js Pages Router behavior for basePath error pages and masked error-route client navigation.
Core change Render pages/404 for production route misses, preserve masked as URLs, and fetch /404 as the component route for router.push('/404', as) and router.push('/_error', as).
Primary files packages/vinext/src/entries/pages-server-entry.ts, packages/vinext/src/server/pages-page-response.ts, packages/vinext/src/shims/router.ts
Expected impact Apps using basePath get custom 404 HTML and Pages Router client transitions hydrate the error component without hard navigating to the masked URL.

Why

For Pages Router navigation, the browser-visible URL and the component route are separate values. Next.js keeps as in history but resolves /404 and /_error as error component routes. Vinext collapsed those values too early, so masked error navigations fetched the friendly URL and production route misses under basePath fell back to generic HTML.

Area Principle / invariant What this PR changes
Production route miss A Pages app with pages/404 should render that route with status 404. Route misses try the explicit /404 page after request normalization and pass the original visible path as asPath.
Client navigation href selects the page component, while as selects the visible browser URL. Pages error-route hrefs fetch /404, preserve the masked history URL, and allow the 404 response to hydrate.
Hydration metadata Client navigation needs module URLs in __NEXT_DATA__ to import the fetched page. Production Pages HTML includes the resolved page and _app module URLs.

What changed

Scenario Before After
/docs/docs/other-page with basePath: '/docs' and pages/404 Generic vinext 404 HTML. Custom pages/404 HTML, status 404, __NEXT_DATA__.page === '/404'.
next.router.push('/404', '/slug-2') under /docs Fetched or hard-navigated to the masked URL. Fetches /docs/404, keeps location /docs/slug-2, renders the error component.
next.router.push('/_error', '/slug-2') under /docs Same collapse of component route and masked URL. Maps the component fetch to /docs/404 while preserving the masked URL.
Validation
  • vp test run tests/shims.test.ts -t "Pages Router (fetches the error route|maps masked /_error)"
  • vp test run tests/pages-router.test.ts -t "(renders pages/404|falls back to pages/_error)"
  • vp test run tests/prerender.test.ts -t "prerenderPages — default mode"
  • vp env exec --node 24 ./scripts/run-nextjs-deploy-suite.sh /Users/nathan/Projects/vinext/.refs/nextjs-v16.2.6 --retries 0 -c 1 --debug test/e2e/basepath/error-pages.test.ts
  • vp check
Maintainer review path
  1. tests/pages-router.test.ts for the production basePath 404 and _error fallback regressions.
  2. tests/shims.test.ts for the ported masked /404 and /_error client navigation contract.
  3. tests/prerender.test.ts for the custom /404 prerender ownership regression.
  4. packages/vinext/src/shims/router.ts for the narrow Pages error-route exception in client navigation.
  5. packages/vinext/src/entries/pages-server-entry.ts and packages/vinext/src/server/pages-page-response.ts for custom 404 rendering, status propagation, asPath, and module metadata.
Risk / compatibility
  • Public API: no new public API.
  • Runtime: the client-side exception is limited to local Pages Router hrefs whose normalized path is /404 or /_error.
  • Status behavior: explicit /404 page renders with status 404, matching the error route contract.
  • Existing app risk: route-miss fallback only invokes an actual /404 page route, not a dynamic route that happens to match /404.

References

Reference Why it matters
Next.js basePath error-pages e2e test Source test for the masked /404 and /_error client navigation behavior.
Next.js Pages Router client router Shows the client router preserving href and as separately and resolving error routes as page components.
Next.js base server error rendering Shows server-side preference for pages/404 before falling back to _error.
Next.js router server miss handling Shows route misses invoking the 404 render path with status 404.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1441

commit: 146343d

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 22, 2026 11:32
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review Summary

Clean, well-scoped fix for basePath error page rendering. The three-pronged approach (server 404 rendering, client error-route fetch URL, prerender dedup) is correct and each piece is independently testable. The tests are well-structured and ported from the right Next.js source.

A couple of observations — one about a missing _error fallback on the server side, and a few minor nits.

Key observations

  1. Server-side: no _error fallback for route misses. The new route-miss block only tries /404. If a Pages app has _error.tsx but no 404.tsx, matchRoute("/404", pageRoutes) returns null and falls through to the generic HTML. Next.js falls back to _error in that case (the base-server.ts reference in the PR description confirms this). The prerender code handles this correctly (hasCustom404 || hasErrorPage), but the server entry does not.

  2. Client-side: resolvePagesErrorHtmlFetchUrl operates on the raw url (href) param, not on as. This is correct — it's the href that selects the component route. Good.

  3. allowNotFoundResponse is a clean extension to navigateClient that avoids changing the existing non-error behavior.

  4. Prerender route.pattern === "/404" skip prevents the double-render issue cleanly.

Overall this is solid work. The _error fallback gap is the main thing worth discussing.

Comment thread packages/vinext/src/entries/pages-server-entry.ts
Comment thread packages/vinext/src/entries/pages-server-entry.ts Outdated
Comment thread packages/vinext/src/shims/router.ts Outdated
Comment thread packages/vinext/src/shims/router.ts Outdated
Comment thread packages/vinext/src/server/pages-page-response.ts
Comment thread packages/vinext/src/entries/pages-server-entry.ts
Comment thread packages/vinext/src/build/prerender.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review posted. Here's a summary of the feedback:

Overall: Solid, well-scoped PR. The three-part fix (server rendering, client fetch URL, prerender dedup) is correct and well-tested.

Main discussion point: The server-side route-miss handler only looks for an explicit pages/404 route. If an app has pages/_error but no pages/404, the matchRoute("/404") call returns null and falls through to generic HTML. The prerender code in this same PR handles both cases (hasCustom404 || hasErrorPage), but the server entry doesn't. Worth discussing whether this is intentional scoping or should be addressed here.

Minor nits:

  • The recursive _renderPage call spreads all original options, which forwards isDataReq — safe today but a subtle invariant. Suggested constructing options explicitly.
  • navigateOptions ternary uses === null where truthy check would be more idiomatic.
  • Noted the resolveSsrManifest + getManifestFilesForModule refactor is a nice cleanup.
  • statusCode correctly takes precedence over gsspRes.statusCode in applyGsspHeaders, matching Next.js behavior.

github run

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review Summary

Clean, well-structured fix. The three-part approach — server 404/_error rendering, client error-route fetch URL resolution, prerender dedup — is correct and each piece is independently testable. The tests are well-ported from the Next.js reference and the PR description is thorough.

I see the previous review's main concern (missing _error fallback on the server side) has been addressed — the generated entry now falls through from /404 to _errorPageRoute at the else if branch. Good.

A few observations below, mostly minor. The one substantive point is about what happens when both the /404 and _error renders themselves throw — currently the outer catch returns a generic 500, which is fine, but worth being aware of.

LGTM overall.

Comment thread packages/vinext/src/entries/pages-server-entry.ts
Comment thread packages/vinext/src/shims/router.ts Outdated
Comment thread packages/vinext/src/shims/router.ts
Comment thread packages/vinext/src/entries/pages-server-entry.ts
Comment thread packages/vinext/src/build/prerender.ts
Comment thread packages/vinext/src/entries/pages-server-entry.ts
Comment thread tests/pages-router.test.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review posted. Here's a summary:

Approved with minor suggestions. The PR is well-structured and correct.

Key findings:

  1. Previous review's _error fallback concern is resolved — the generated entry now correctly falls through from /404 match to _errorPageRoute in the else if branch at line 532.

  2. The matchRoute("/404") + pattern === "/404" guard is important — without the pattern check, a dynamic catch-all route could match /404. Suggested adding an inline comment explaining why.

  3. The /_error/404 client fetch mapping chain is clever — when a server only has _error, the client fetches /404 which triggers the server's own route-miss → _errorPageRoute fallback. The end-to-end flow works correctly.

  4. resolveSsrManifest + getManifestFilesForModule refactor is a nice cleanup that deduplicates the manifest lookup logic.

  5. statusCode prop injection correctly scoped to /_error only, matching Next.js behavior.

  6. Prerender status === 404 guard prevents misconfigured 404 pages from being written as 404.html.

  7. Test coverage is solid — both server-side (basePath 404, _error fallback) and client-side (masked /404 and /_error navigations) are covered, with proper Next.js test provenance comments.

github run

Pages Router route misses under basePath returned generic HTML and masked client navigations to /404 or /_error could fetch the visible as-path instead of the error route. This diverged from Next.js when applications used next/router to preserve a friendly URL while rendering the error component.

The implementation assumed the browser URL and the component route could always collapse to one fetch target. Preserve the masked history URL, fetch the Pages error route as the HTML target, allow its 404 response to hydrate, and render pages/404 for production route misses with the correct status and client module metadata.
Vitest integration CI does not install Playwright browsers, so launching Chromium from tests/pages-router.test.ts made the PR fail deterministically even though the upstream deploy harness passed.

Keep the browser-level verification in the upstream deploy-suite run and cover vinext locally at the lower boundaries: production 404 rendering through HTTP and Pages Router masked error-route navigation through the router shim. The generated Pages entry now also shares one SSR manifest module lookup helper between asset tag collection and client module URL resolution.
The pattern check prevents a dynamic catch-all route (e.g. [...slug]) from
being used as the 404 fallback. Without it, matchRoute can return a
catch-all match and silently hijack the error rendering path.
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/basepath-error-pages branch from 7260386 to feb464a Compare May 22, 2026 13:27
@NathanDrake2406 NathanDrake2406 marked this pull request as draft May 22, 2026 13:42
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 22, 2026 13:54
…or-pages

# Conflicts:
#	packages/vinext/src/entries/pages-server-entry.ts
#	packages/vinext/src/shims/router.ts
Copilot AI review requested due to automatic review settings May 22, 2026 17:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aligns Vinext’s Pages Router error-route behavior with Next.js when basePath and masked client navigations are involved, ensuring custom pages/404 renders for production misses and that client-side router.push('/404', as) / router.push('/_error', as) fetch the correct component route while preserving the masked URL.

Changes:

  • Server: on production route misses, prefer rendering an explicit pages/404 (or fall back to pages/_error) with status 404, and pass the missed path through as asPath.
  • Client router shim: introduce a narrow exception so error-route navigations fetch /404 HTML (optionally allowing 404 responses) while keeping the masked as URL in history.
  • Hydration metadata: embed resolved page and _app module URLs into __NEXT_DATA__.__vinext so error-route HTML navigations can hydrate without hard navigation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/shims.test.ts Adds coverage for masked /404 and /_error client navigation under basePath (including locale-prefixed paths).
tests/prerender.test.ts Ensures only one /404 prerender result is emitted.
tests/pages-router.test.ts Adds prod-server regression tests for basePath route-miss 404 rendering and fallback rewrite ordering.
packages/vinext/src/shims/router.ts Routes masked error navigations to fetch /404 HTML while preserving as, and allows 404 HTML responses for this path.
packages/vinext/src/server/prod-server.ts Defers custom error rendering on initial miss to allow fallback rewrites, then renders error page after rewrites if still missing.
packages/vinext/src/server/pages-page-response.ts Adds status override support and embeds __vinext module URL metadata into __NEXT_DATA__.
packages/vinext/src/entries/pages-server-entry.ts Adds _error route awareness, improves miss handling to render /404 (or _error), propagates status/asPath, and emits module URL metadata.
packages/vinext/src/deploy.ts Mirrors prod-server miss handling logic in the Worker entry (defer error page until after fallback rewrites).
packages/vinext/src/build/prerender.ts Skips /404 in the generic prerender loop and renders it via the dedicated 404 block expecting a 404 status.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/vinext/src/server/prod-server.ts
Comment thread packages/vinext/src/deploy.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread packages/vinext/src/deploy.ts
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Addressed the ISR status review in e10bfcb. The Pages ISR cache now stores the rendered status from renderPagesPageResponse, cached HIT/STALE responses reuse the cached status with a 200 fallback for legacy entries, and background regeneration preserves the render status as well. Added regression coverage for a revalidating custom pages/404 route miss: first MISS and second HIT both return 404 with the custom 404 HTML.

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Addressed the stale ISR metadata review in 146343d. resolvePagesPageData now receives the same __vinext module metadata as normal HTML rendering, and renderPagesIsrHtml() passes it into the regenerated __NEXT_DATA__ script. The generated Pages entry computes pageModuleUrl / appModuleUrl before page-data resolution so stale background regeneration can preserve them. Added a focused STALE regression that runs regeneration and asserts the rewritten cached HTML still includes __vinext.pageModuleUrl and appModuleUrl.

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.

3 participants