Skip to content

fix(pages): preserve missing page props on errors#2568

Merged
james-elicx merged 19 commits into
mainfrom
codex/fix-pages-no-page-props-28938793088
Jul 9, 2026
Merged

fix(pages): preserve missing page props on errors#2568
james-elicx merged 19 commits into
mainfrom
codex/fix-pages-no-page-props-28938793088

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • use the framework next/error page as the default Pages Router /_error route in production and client loader manifests
  • preserve a custom _app.getInitialProps result that intentionally omits pageProps instead of synthesizing error props into it
  • align dev error rendering with production by loading the framework error page and running custom _app.getInitialProps
  • add the missing default error-page getInitialProps and client-exception rendering semantics

Root cause

Vinext returned a hand-authored 404 document whenever an app did not define pages/404 or pages/_error. That bypassed the app's custom _app, so the no-page-props fixture never exercised the same error render as Next.js. The production handler also unconditionally created pageProps.statusCode, masking a custom _app.getInitialProps result that omitted pageProps.

Next.js parity

Fixes run 28938793088 against Next.js v16.3.0-canary.80 (9dd6d677b63b249584c8ff0bccffcc6a65288683):

  • test/e2e/no-page-props/no-page-props.test.ts
    • should load 404 page correctly
    • should navigate between pages correctly

Reference:

  • test/e2e/no-page-props/no-page-props.test.ts
  • packages/next/src/shared/lib/utils.ts (loadGetInitialProps)
  • packages/next/src/pages/_error.tsx

Validation

  • vp test run tests/pages-page-handler.test.ts tests/shims.test.ts tests/entry-templates.test.ts — 1,256 passed
  • vp test run tests/pages-router.test.ts -t "falls back to pages/_error for route misses" — 1 passed
  • vp test run tests/pages-router.test.ts -t "routes throwing .* through the error page once in dev|falls back to pages/_error for route misses" — 3 passed
  • targeted vp check for all 8 changed files — clean
  • REPO="$PWD" NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref-v16.3.0-canary.80" ./scripts/run-targeted-nextjs-e2e.sh test/e2e/no-page-props/no-page-props.test.ts — 5 passed, 0 failed

Bonk has not been requested yet; this is ready for independent review.

@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2568
npm i https://pkg.pr.new/create-vinext-app@2568
npm i https://pkg.pr.new/vinext@2568

commit: 79074bf

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 79074bf against base 5554817 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 126.6 KB 126.6 KB ⚫ -0.0%
Client entry size (gzip) vinext 120.6 KB 120.6 KB ⚫ -0.0%
Dev server cold start vinext 2.86 s 2.84 s ⚫ -0.7%
Production build time vinext 3.06 s 3.07 s ⚫ +0.2%
RSC entry closure size (gzip) vinext 98.5 KB 98.5 KB ⚫ -0.0%
Server bundle size (gzip) vinext 165.2 KB 165.2 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

UnknownError: The operation timed out.

github run

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

Readiness is blocked on exact head de064d8c80c16c48552bd6a15b9d7349d709fc5c by two items:

  1. Deterministic CI type failure: Check fails with TS2540: Cannot assign to NODE_ENV because it is a read-only property at packages/vinext/src/index.ts:1884. This is induced by the new runtime import type { NextPageContext } from "next" in packages/vinext/src/shims/error.tsx, which loads the installed Next.js declarations and their readonly ProcessEnv.NODE_ENV augmentation into vinext source compilation. The narrow remediation is to remove that runtime import and type getErrorInitialProps with a local structural context type; retain the public NextPageContext signatures in next-shims.d.ts and the compile regression. Then rerun targeted check/build and the exact no-page-props suite.

  2. Bonk infrastructure: run 28967189681 failed after 6m with UnknownError: The operation timed out, before producing a review verdict. Do not treat it as a review finding; retrigger only after the CI remediation is pushed and independently reviewed.

PR remains draft; all other CI jobs are green.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

UnknownError: The operation timed out.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Reviewed the diff against Next.js packages/next/src/pages/_error.tsx (canary). The port is faithful and the fix correctly addresses the root cause. Ran vp check on all 6 changed source files (clean) and the targeted suites (tests/shims.test.ts -t "next/error shim" → 25 passed, tests/pages-page-handler.test.ts + tests/entry-templates.test.ts → 67 passed, tests/pages-router.test.ts -t "falls back to pages/_error for route misses" → 1 passed).

Overall this is solid, well-tested work with strong Next.js parity. The findings below are minor.

1. No end-to-end prod test for the framework error page when no _error/404/500 exists (medium)

The new behavior — serving next/error as the default /_error route in production — is only unit-tested at the string level (tests/entry-templates.test.ts asserts the generated import * as ErrorPageModule from "next/error" and "/_error": () => import("next/error") lines). The existing falls back to pages/_error for route misses prod build test still uses a real pages/_error.tsx, so it does not exercise the new no-file path through a full buildPagesFixtureToOutDir + startProdServer flow.

Worth adding a prod build test that omits _error/404/500 and asserts a 404/500 renders the framework error document, so the framework-error-page path is covered end-to-end (including client hydration).

2. Framework error route has filePath: null, so no preload/<script> tag for its client chunk (low)

pages-server-entry.ts:304 builds _errorPageRoute with filePath: ${errorAssetPathJson}, which is null when no user _error file exists. Downstream:

  • pages-page-handler.ts:759 (if (route.filePath) pageModuleIds.push(route.filePath)) skips the framework error module, so collectAssetTags emits no preload/script for it.
  • pages-page-handler.ts:512-514 resolveClientModuleUrl(manifest, null, ...) returns undefined.

Client hydration should still work because pages-client-entry.ts:106 registers "/_error": () => import("next/error") in the loader map, so the browser dynamically imports the framework chunk at hydration. But this differs from a user-authored _error (which gets a preload) and adds a round trip. This appears to be acceptable/intended, but the lack of an integration test (finding 1) means prod hydration of the framework error page is unverified. Please confirm hydration works in a real prod build.

3. Dev/prod divergence in how a custom _app omitting pageProps is handled (low — confirm intended)

  • Prod (pages-page-handler.ts:663-670) guards with renderProps.pageProps !== undefined before injecting statusCode, preserving an _app.getInitialProps result that intentionally omits pageProps.
  • Dev (dev-server.ts:2129-2153) fully replaces renderProps with appInitialProps whenever _app has getInitialProps, and only injects statusCode into errorProps when _app has none.

Both paths preserve the "missing pageProps" case, but via different mechanisms. The no-page-props e2e passes, so this is likely fine — flagging so the parity is a deliberate choice rather than incidental.

Nits

  • error.tsx:41 and next-shims.d.ts both use the err.statusCode! non-null assertion, so getInitialProps({ err: {} }) returns statusCode: undefined even though ErrorProps.statusCode is typed number. This matches Next.js exactly (same !), and the added test asserts it — no change needed, just noting the inherited unsoundness.
  • The two ErrorPageContext shapes diverge intentionally: the runtime one in error.tsx ({ err?, req?, res? }) is a narrow subset of the .d.ts NextPageContext. Fine given the asNextPageContext cast in tests, but the duplicated-but-different name could confuse future readers.

Nice work on the hostname fallback (req.urlhost header) — verified path-only and protocol-relative URLs throw and are safely caught, so there's no crash risk on typical Node path-only req.url.

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I reviewed PR #2568 (fix(pages): preserve missing page props on errors) against the Next.js _error.tsx reference and ran the targeted test suites. The port is faithful and the fix correctly addresses the stated root cause. vp check on all 6 changed source files is clean, and all targeted tests pass (25 error-shim + 67 handler/template + 1 pages-router fallback).

Findings (all minor — no blockers):

  1. No end-to-end prod test for the framework error page when no _error/404/500 file exists (medium). The new "serve next/error as default /_error" behavior is only unit-tested at the generated-string level. The existing prod build test still uses a real _error.tsx, so the new no-file path isn't exercised through a full build + prod server.

  2. Framework error route has filePath: null → no preload/<script> tag for its chunk (low). Hydration should still work via the client loader map's import("next/error"), but it differs from user-authored _error and is unverified end-to-end (ties to finding 1).

  3. Dev/prod use different mechanisms to preserve a custom _app that omits pageProps (low). Both preserve the case correctly; flagged to confirm it's a deliberate parity choice.

Plus two nits (inherited statusCode! unsoundness that matches Next.js exactly, and the two divergent ErrorPageContext shapes). I also verified the hostname req.urlhost header fallback has no crash risk on path-only or protocol-relative URLs.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Bonk completed on exact head af70356935df482984cdb34ab1a90c946f0bfb67 with no correctness blocker, but one actionable verification gap remains before readiness:

  • Add a real production build/server regression with no user _error, 404, or 500 page, exercising the framework next/error fallback end to end and confirming client hydration. This should also verify the filePath: null/dynamic client-loader path does not leave the page unhydrated.

The dev/prod implementation difference for preserving an _app result without pageProps is intentional: dev receives and preserves the custom _app.getInitialProps envelope directly, while production conditionally augments an existing pageProps object; both avoid synthesizing pageProps and the pinned no-page-props suite passes 5/5.

All 64 exact-head CI checks are green and merge state is CLEAN, but I am leaving the PR draft until the production fallback/hydration regression is added and independently reviewed.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Re-reviewed the exact head (4ea7045) after the production hydration test was added. I diffed shims/error.tsx against the Next.js packages/next/src/pages/_error.tsx and _app.tsx references, traced the dev/prod status-code flow, and re-ran the targeted suites. The fix is correct and the earlier "no end-to-end prod fallback test" gap is now closed by tests/e2e/pages-router-prod/default-error.browser.spec.ts, which builds a fixture with no _error/404/500, asserts 404/500 render the framework next/error document, and confirms hydration (waitForHydration) with no unexpected console errors.

Verification I ran (all green):

  • vp check on the 5 changed source files + the 4 changed/added test files — clean
  • tests/shims.test.ts -t "next/error shim" → 25 passed
  • tests/pages-page-handler.test.ts + tests/entry-templates.test.ts → 67 passed
  • tests/pages-router.test.ts -t "falls back to pages/_error for route misses" → 1 passed

The res.statusCode = renderStatusCode set before getInitialProps (prod createPageReqRes, dev line 2118) is the right mechanism — it feeds the framework error page's getErrorInitialProps the correct status, matching Next.js where the response status is set before the error render. I confirmed the final HTTP status comes from the explicit statusCode: renderStatusCode passed to renderPagesPageResponse, not the mutated reqRes.res.statusCode, so there's no risk of the mutation leaking into the wrong response status.

Findings below are minor — no blockers.

1. Dev-path renderErrorPage change has no direct dev integration test (low)

The prod fallback path is now covered end-to-end, but the symmetric dev-server change in server/dev-server.ts:2094-2154 (loading next/error when no _error file exists via errorAssetPath ?? "next/error", plus running a custom _app.getInitialProps and preserving an envelope that omits pageProps) is only exercised indirectly. The PR description references tests/pages-router.test.ts -t "routes throwing .* through the error page once in dev", but no test matching that name exists on this branch (grep for error page once in dev / renderErrorPage finds only pages-page-handler.test.ts and pages-request-pipeline.test.ts, neither of which covers the new dev framework-error/_app.getInitialProps path). Given the repo's dev/prod parity rule (AGENTS.md), a focused dev integration test for the no-_error-file + custom-_app case would lock in the parity the prod test now guards. I traced the dev logic and believe it's correct (see below), so this is a coverage gap, not a defect.

2. Dead-ish branch in pages-server-entry.ts (nit)

ErrorPageModule is now import * as ErrorPageModule from "next/error" in the no-user-file case, so it's always a truthy module namespace. The generated _errorPageRoute = ErrorPageModule ? {...} : null ternary (pages-server-entry.ts:304) can therefore never take the null branch anymore. Harmless, but the : null fallback is now unreachable — worth simplifying or dropping a comment so a future reader doesn't assume the null path is still live.

3. Wasted ErrorComponent.getInitialProps call in dev when custom _app supplies props (nit)

In dev-server.ts, initialErrorProps/errorProps (line 2119-2128) are computed unconditionally, then discarded whenever AppComponent has getInitialProps and returns an envelope (renderProps = appInitialProps, line 2153). Correct behavior — a well-behaved _app that calls App.getInitialProps({ Component: ErrorComponent, ctx }) re-runs the error component's getInitialProps and gets statusCode from the now-set res.statusCode, matching Next.js _app.tsx's loadGetInitialProps(Component, ctx). But the eager compute is redundant on that branch; consider computing errorProps lazily only for the no-_app-getInitialProps path.

Verified, no action needed

  • statusCode not dropped in dev. For the three non-no-page-props combinations (no _app, _app without gIP, _app with gIP that returns { pageProps }), statusCode reaches the rendered error props. Only the intentional no-page-props envelope omits it, which is the documented fix. No regression.
  • err.statusCode! unsoundness in error.tsx:41 / next-shims.d.ts (typed statusCode: number but returns undefined for err: {}) matches Next.js exactly and is asserted by the new test.
  • Hostname fallback (req.urlHost header) safely try/catches path-only and protocol-relative URLs; the new test covers both the absolute-URL and Host-header branches.
  • Prod renderProps.pageProps !== undefined guard correctly preserves a custom _app envelope without pageProps while still injecting statusCode for user _error pages that didn't set it.

Solid, well-tested work with strong Next.js parity. The only thing I'd add before calling it done is finding #1 (a dev integration test to match the new prod one).

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I re-reviewed the exact head (4ea7045) against the Next.js _error.tsx/_app.tsx references, traced the dev/prod status-code flow, and re-ran the targeted suites. The fix is correct and the main gap from the prior review is now closedtests/e2e/pages-router-prod/default-error.browser.spec.ts builds a fixture with no _error/404/500, renders the framework next/error document for 404/500, and verifies client hydration end-to-end.

Verification (all green): vp check on all 9 changed files, 25 error-shim tests, 67 handler/entry-template tests, 1 pages-router fallback test.

Findings (all minor — no blockers):

  1. Dev-path renderErrorPage change has no direct dev integration test (low). The prod fallback is now covered, but the symmetric dev-server change (dev-server.ts:2094-2154) is only exercised indirectly. The PR body references a "error page once in dev" test that doesn't exist on this branch. Given AGENTS.md's dev/prod parity rule, a focused dev test would lock in the parity.

  2. Dead : null branch in pages-server-entry.ts:304 (nit). ErrorPageModule is now always a truthy namespace import, so the ? ... : null fallback is unreachable.

  3. Redundant eager errorProps compute in dev (nit). Discarded whenever a custom _app supplies props.

Verified with no action needed: statusCode is not dropped in dev (only the intentional no-page-props envelope omits it), the err.statusCode! unsoundness matches Next.js exactly, the hostname fallback safely handles path-only/protocol-relative URLs, and the final HTTP status comes from the explicit statusCode arg (not the mutated reqRes.res.statusCode), so the pre-render status mutation can't leak into the wrong response.

Solid, well-tested work with strong Next.js parity. The only follow-up worth doing before calling it done is finding #1.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Exact-head readiness remains blocked at 4ea704559b67b14f147ab71035753ce8c150574d by the latest Bonk review.

Bonk confirms the implementation is correct, the production framework-error/hydration gap is closed, all 64 CI checks are green, and merge state is CLEAN. It nevertheless identifies one actionable pre-readiness item: add a focused dev integration regression for the no-user-_error path with a custom _app.getInitialProps, verifying the framework next/error fallback and preservation of an envelope that omits pageProps.

The other two findings (the now-unreachable _errorPageRoute null branch and eager dev error-props computation) are nits, not blockers. Leaving the PR draft pending the dev regression, fresh independent review, and Bonk retrigger.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Reviewed the exact head (02ce049) against Next.js packages/next/src/pages/_error.tsx (v16.3.0-canary.80), traced the dev/prod status-code and SSR-context flow, and re-ran the targeted suites. The fix is correct and the previously-flagged dev integration gap is now closedtests/e2e/pages-router-prod/default-error-dev.browser.spec.ts exercises the no-_error-file + custom-_app.getInitialProps path end-to-end, including the concurrent-request isolation case.

Verification I ran (all green):

  • tests/shims.test.ts -t "next/error shim" → 25 passed
  • tests/pages-page-handler.test.ts + tests/entry-templates.test.ts → 67 passed

The next/error port is faithful: statusCodes table, styles, the getInitialProps/origGetInitialProps statics, the displayName, and the client-exception <h2> branch all match upstream. The hostname divergence (req.url/Host header vs Next.js's getRequestMeta(req, 'initURL')) is a reasonable, documented substitute and only affects the client-exception (falsy-statusCode) message, which never renders server-side. The concurrency fix is well-designed: the route-miss caller (dev-server.ts:722-740) is correctly wrapped in its own runWithRequestContext because it sits before the main request's ALS scope at line 763, while the matched-route callers inherit that outer scope — so the finally { setSSRContext(null) } cleanup is always per-request-isolated. The default-error-dev concurrency test (/missing-a with a 50ms delay racing /missing-b) locks this in.

Findings below are all minor — no blockers.

1. Dev framework-error fallback tail is now effectively unreachable (low)

renderErrorPage's post-loop fallback (dev-server.ts:2375-2391) renders the hand-authored 404/500 body when no candidate matched. But the _error candidate now imports errorAssetPath ?? "next/error" (line 2127), and next/error always exposes a truthy default, so the loop reaches its return on the _error candidate for every 404/500/other status. The tail is only reachable if the next/error import itself throws (caught at line 2364). That makes it a genuine safety net rather than dead code, but the comment above it still describes it as the "no custom error page found" path, which no longer matches reality. Worth a one-line comment update so a future reader doesn't assume the default-body path is the normal no-_error behavior (it isn't — next/error is).

2. Unreachable : null branch in pages-server-entry.ts:304 (nit — still open)

Same nit Bonk flagged on the prior head and still unaddressed: ErrorPageModule is now always import * as ErrorPageModule from "next/error" (or the user file) in the generated server entry, so _errorPageRoute = ErrorPageModule ? {...} : null can never take the null branch. Harmless, but the : null fallback is dead — simplify or drop a comment.

3. Eager errorProps compute discarded on the custom-_app branch (nit — still open)

In dev-server.ts, initialErrorProps/errorProps (lines 2166-2175) are computed unconditionally, then renderProps is replaced with appInitialProps whenever _app has getInitialProps (line 2199). Correct behavior (a well-behaved _app re-runs the error component's gIP via AppTree/Component), but the eager compute is wasted on that branch. Consider computing lazily only for the no-_app-gIP path.

Verified, no action needed

  • statusCode flow (prod): createPageReqRes sets res.statusCode = renderStatusCode before getInitialProps (pages-page-handler.ts:568), so the framework error's getErrorInitialProps reads the correct 404/500. The final HTTP status still comes from the explicit statusCode: renderStatusCode arg, not the mutated res.statusCode.
  • renderProps.pageProps !== undefined guard correctly preserves a custom _app envelope that omits pageProps while still injecting statusCode for user _error pages.
  • forceReady=true on _initializePagesRouterReadyFromNextData (router.ts:1369, dev script line 2269) defaults to false, so existing callers are unaffected; the error page needs it because /_error won't satisfy getPagesNavigationIsReadyFromSerializedState against the real URL. Matches Next.js's ready-on-error-page behavior.
  • err.statusCode! unsoundness in error.tsx:41 matches Next.js exactly and is asserted by the new getInitialProps({ err: {} }) test.
  • Server/client hydration parity for the no-pageProps envelope: both server render and the dev hydration script (line 2283) pass pageProps: undefined to the custom _app, so hydration is consistent.

Solid, well-tested work with strong Next.js parity. The remaining items are all cosmetic; I see no correctness blocker.

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I reviewed the exact head (02ce049) against the Next.js _error.tsx reference (v16.3.0-canary.80), traced the dev/prod status-code and SSR-context flow, and ran the targeted suites (25 error-shim + 67 handler/entry-template tests, all green).

The fix is correct and the previously-flagged dev integration gap is now closed by default-error-dev.browser.spec.ts, which covers the no-_error-file + custom-_app.getInitialProps path end-to-end including concurrent-request isolation.

Key things I verified:

  • The next/error port (status table, styles, getInitialProps/origGetInitialProps statics, displayName, client-exception <h2> branch) matches upstream faithfully.
  • The concurrency fix is well-designed: the route-miss caller is correctly wrapped in its own runWithRequestContext because it sits before the main request's ALS scope, so the finally { setSSRContext(null) } cleanup is per-request-isolated. The /missing-a (50ms delay) vs /missing-b race test locks this in.
  • statusCode flows correctly (res.statusCode set before getInitialProps, final HTTP status from the explicit arg), the pageProps !== undefined guard preserves the no-page-props envelope, and server/client hydration are consistent.

Findings (all minor — no blockers):

  1. (low) Dev fallback tail (dev-server.ts:2375-2391) is now effectively unreachable since the _error candidate always resolves to next/error; its comment still describes it as the normal no-_error path. Update the comment.
  2. (nit, still open) Unreachable : null branch in pages-server-entry.ts:304ErrorPageModule is always truthy now.
  3. (nit, still open) Eager errorProps compute in dev is discarded on the custom-_app branch.

No correctness blocker. Solid, well-tested work with strong Next.js parity.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Re-reviewed the new exact head (4234482), which lands two follow-up commits on top of the last-reviewed 02ce049. The delta is small and addresses the two open nits from the prior review. The fix remains correct; both previously-open nits are now resolved, and no new issues were introduced.

Verification I ran (all green):

  • vp check packages/vinext/src/server/dev-server.ts packages/vinext/src/entries/pages-server-entry.ts — clean (format + lint + type)
  • vp test run tests/pages-page-handler.test.ts tests/shims.test.ts tests/entry-templates.test.ts → 1,265 passed

What changed since 02ce049

1. pages-server-entry.ts:304 — dead : null branch removed (prior nit #2, now fixed).
_errorPageRoute is now an unconditional object literal instead of ErrorPageModule ? {...} : null. Since ErrorPageModule is always a truthy namespace import (user file or import * as ErrorPageModule from "next/error"), the ternary was unreachable. Correct simplification.

2. dev-server.ts renderProps restructure (prior nit around the discarded initializer).
renderProps is no longer eagerly initialized to { pageProps: errorProps }. It's now assigned in exactly one of two branches:

  • _app with getInitialProps: renderProps = appInitialProps ?? {}
  • otherwise: renderProps = { pageProps: errorProps }

I traced the downstream consumers and confirmed parity is preserved:

  • Server render (createErrorElement, line 2223): when FinalApp is falsy it renders createElement(FinalComponent, errorProps) (statusCode present); when FinalApp exists it spreads renderProps.
  • __NEXT_DATA__ (line 2254) serializes renderProps, and the hydration script (lines 2285-2286) reads props.pageProps for both the _app and no-_app branches. Server and client render the same props in all three non-no-page-props combinations, and the intentional no-page-props envelope (appInitialProps without pageProps) is preserved verbatim — matching the prod renderProps.pageProps !== undefined guard in pages-page-handler.ts:663-670.
  • TS definite-assignment holds (both branches assign; early returns on res.headersSent occur before first use).

3. Dev fallback-tail comment updated (prior review finding #1, now fixed).
The comment at dev-server.ts:2377 now correctly describes the post-loop fallback as a defensive net for a missing/invalid framework error module, since the _error candidate always resolves next/error (which always exposes a truthy default) inside the loop. The stale "no custom error page found → default body" comment is gone.

Remaining item (nit — non-blocking, unchanged)

errorProps/initialErrorProps (dev-server.ts:2166-2175) are still computed eagerly and discarded on the custom-_app-with-gIP branch (that branch re-runs the error component's gIP via App.getInitialProps({ Component: ErrorComponent, ctx })). Behavior is correct; the eager compute is a cheap, harmless redundancy. Not worth a follow-up on its own.

Verified, no action needed (carried from prior reviews, re-confirmed on this head)

  • next/error port fidelity (status table, styles, getInitialProps/origGetInitialProps statics, client-exception <h2> branch).
  • statusCode flow: res.statusCode set before getInitialProps; final HTTP status from the explicit statusCode arg, not the mutated res.statusCode.
  • Concurrency isolation (route-miss caller wrapped in its own runWithRequestContext; finally { setSSRContext(null) } per-request-isolated) — covered by the default-error-dev race test.
  • err.statusCode! unsoundness matches Next.js exactly and is asserted by tests.

No correctness blocker. The two open nits from the last pass are addressed and the change is safe. This looks ready.

@ask-bonk

ask-bonk Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I re-reviewed the new exact head (4234482), which adds two follow-up commits on top of the last-reviewed 02ce049. The delta closes the two open nits from the prior review and introduces no new issues.

Verification (all green):

  • vp check on both changed source files — clean
  • vp test run tests/pages-page-handler.test.ts tests/shims.test.ts tests/entry-templates.test.ts → 1,265 passed

What changed since last review:

  1. pages-server-entry.ts:304 — removed the now-unreachable : null branch on _errorPageRoute (prior nit Published npm package ships sourcemaps pointing to missing source files #2, fixed).
  2. dev-server.tsrenderProps no longer eagerly initialized; assigned in exactly one of two branches. I traced server render, __NEXT_DATA__ serialization, and client hydration to confirm dev/prod parity holds and the intentional no-page-props envelope is preserved verbatim.
  3. dev-server.ts:2377 — the stale fallback-tail comment is corrected to describe it as a defensive net (prior finding Virtual module imports break esbuild dependency optimization when vinext is installed from npm #1, fixed).

Remaining: one unchanged nit (eager errorProps compute discarded on the custom-_app branch) — cheap and harmless, non-blocking.

No correctness blocker. The change is safe and looks ready.

github run

@james-elicx james-elicx marked this pull request as ready for review July 8, 2026 20:08
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Re-reviewed the exact head (8876103, which merges main into the branch). I diffed shims/error.tsx against Next.js packages/next/src/pages/_error.tsx (v16.3.0-canary.80), re-traced the dev/prod status-code and SSR-context flow, and re-ran the targeted suites. Prior reviews focused on dev/prod render behavior; this pass turned up one real client/server parity bug in the production client entry that the earlier passes missed.

Verification I ran (all green):

  • vp check on all 7 changed source files — clean
  • vp test run tests/pages-page-handler.test.ts tests/shims.test.ts tests/entry-templates.test.ts → 1,265 passed

1. Custom pages/_error is ignored by the production client loader map (medium — correctness)

pages-client-entry.ts:105-107 adds a hardcoded framework loader for /_error:

if (!pageRoutes.some((route) => route.pattern === "/_error")) {
  loaderEntries.push('  "/_error": () => import("next/error")');
}

The guard is intended to skip the framework loader when the app ships a custom _error, but it never fires: _error is a reserved page name (RESERVED_PAGE_NAMES in routing/pages-router.ts:25) and is excluded from pageRoutes by design, so pageRoutes.some(r => r.pattern === "/_error") is always false. The next/error loader is therefore pushed unconditionally — even when the user has pages/_error.tsx.

I confirmed this empirically by generating the client entry for a fixture with a custom pages/_error.tsx:

=== /_error loader lines (custom _error present) ===
  "/_error": () => import("next/error")

Impact: window.__VINEXT_PAGE_LOADERS__["/_error"] maps to the framework component. On client-side navigation that resolves to /_error, shims/router.ts:2342-2349 prefers the loader map over the serialized pageModuleUrl ("Prefer the generated route loader when it exists, even on the HTML fallback path"). So an app with a custom _error would client-navigate into the framework next/error page instead of its own — a client/server divergence (the server entry correctly uses the user's errorFilePath at pages-server-entry.ts:94-98; initial hydration also uses the correct pageModuleUrl, so only client-side nav to /_error is affected).

This is also a dev/prod parity gap: the dev path resolves the custom file correctly (dev-server.ts:2246errorAssetPath ? createPagesDevModuleUrl(...) : "next/error"), so dev and prod would load different /_error components for the same custom-error app.

Suggested fix: resolve the user file the same way the other entries do and only fall back to the framework component when it's absent. findFileWithExts is already imported (used for _app at line 67):

const errorFilePath = findFileWithExts(pagesDir, "_error", fileMatcher);
loaderEntries.push(
  errorFilePath
    ? `  "/_error": () => import(${JSON.stringify(errorFilePath)})`
    : `  "/_error": () => import("next/error")`,
);

The existing entry-templates.test.ts case only covers the _error-absent path (clientCode contains import("next/error")). A companion assertion for the custom-_error case (loader points to the user file, not next/error) would lock this in.

2. Server/client prop shape differs for the no-pageProps envelope in dev (low)

In dev, for a custom _app that omits pageProps:

  • Server render (dev-server.ts:2223-2228): createElement(FinalApp, { ...renderProps, Component, router }) → no pageProps key at all.
  • Hydration script (dev-server.ts:2285): React.createElement(AppComponent, { ...props, Component, pageProps: props.pageProps, router })pageProps: undefined explicitly present.

The default-error-dev e2e passes and reports no console errors, so React tolerates this (a missing key vs undefined read the same when the app destructures pageProps). Flagging only for symmetry — the prod path passes the same renderProps on both sides.

Verified, no action needed (re-confirmed on this head)

  • next/error port fidelity: statusCodes table, styles, getInitialProps/origGetInitialProps statics, displayName, client-exception <h2> branch all match upstream. The hostname substitution (req.urlHost header vs Next.js's getRequestMeta(req, 'initURL')) safely try/catches path-only and protocol-relative URLs.
  • statusCode flow: res.statusCode set before getInitialProps; final HTTP status comes from the explicit statusCode arg, not the mutated res.statusCode.
  • Prod renderProps.pageProps !== undefined guard (pages-page-handler.ts:663-670) correctly preserves a custom _app envelope without pageProps while still injecting statusCode for the framework/default error page — covered by the new unit test.
  • Concurrency isolation: the route-miss caller is wrapped in its own runWithRequestContext (it sits before the outer request ALS scope at dev-server.ts:762), so finally { setSSRContext(null) } is per-request-isolated. The /missing-a vs /missing-b race test locks this in.
  • err.statusCode! unsoundness matches Next.js exactly and is asserted by tests.
  • next-shims.d.ts declarations match the runtime; the ErrorPageContext (.d.ts, full NextPageContext shape) vs the narrow runtime { err?, req?, res? } divergence is intentional.

Finding #1 is a genuine correctness/parity bug for apps with a custom pages/_error.tsx and should be fixed before merge. The rest of the change is solid, well-tested work with strong Next.js parity.

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I re-reviewed the exact head (8876103) and found one real correctness/parity bug that the four prior review passes missed:

Finding #1 (medium — correctness): In pages-client-entry.ts:105-107, the guard meant to skip the framework /_error loader when an app ships a custom pages/_error.tsx never fires. _error is a reserved page name excluded from pageRoutes by design, so pageRoutes.some(r => r.pattern === "/_error") is always false, and () => import("next/error") is pushed unconditionally.

I confirmed empirically that generating the client entry for a fixture with a custom pages/_error.tsx still emits "/_error": () => import("next/error"). Because shims/router.ts:2342-2349 prefers the loader map over the serialized pageModuleUrl, an app with a custom _error client-navigates into the framework component instead of its own — diverging from both the server entry (which correctly uses errorFilePath) and the dev path (which resolves the custom file). I supplied a suggested fix using the already-imported findFileWithExts.

Also flagged one low-severity dev prop-shape asymmetry (missing pageProps key on server vs explicit pageProps: undefined on client), and re-confirmed the items prior reviews verified (next/error port fidelity, statusCode flow, concurrency isolation, the prod pageProps !== undefined guard).

Verification: vp check clean on all 7 changed files; 1,265 targeted tests passing. Finding #1 should be fixed before merge.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bonk review — PR #2568

Re-reviewed the exact head (67d074f1, fix(pages): load custom error page on navigation), which lands one follow-up commit on top of the last-reviewed 8876103. The single medium-severity correctness bug from the prior review (finding #1 — custom pages/_error ignored by the production client loader map) is now fixed, and the follow-up introduces no new issues.

Verification (all green)

  • vp check packages/vinext/src/entries/pages-client-entry.ts tests/entry-templates.test.ts tests/e2e/pages-router-prod/custom-error-navigation.browser.spec.ts — clean (format + lint + type)
  • vp test run tests/entry-templates.test.ts tests/pages-page-handler.test.ts tests/shims.test.ts → 1,266 passed

What changed since 8876103

1. pages-client-entry.ts:106-110 — custom _error now honored (prior finding #1, fixed).
The unreachable pageRoutes.some(r => r.pattern === "/_error") guard is replaced with a real file resolution:

const errorFilePath = findFileWithExts(pagesDir, "_error", fileMatcher);
loaderEntries.push(
  errorFilePath !== null
    ? `  "/_error": () => import(${JSON.stringify(errorFilePath)})`
    : '  "/_error": () => import("next/error")',
);

This is resolved identically to the server entry (pages-server-entry.ts:77), so the client loader map (window.__VINEXT_PAGE_LOADERS__["/_error"]) now points to the user file when present. Since shims/router.ts:2342-2349 prefers the loader map on client navigation, an app with a custom pages/_error.tsx now client-navigates into its own component instead of the framework next/error. This also restores dev/prod parity — the dev path (dev-server.ts:2246-2248, errorModuleSource) already resolves the custom file the same way.

I re-confirmed the fix empirically: the added entry-templates.test.ts case generates the client entry for a fixture with a custom _error.page.tsx (under pageExtensions: ["page.tsx", "tsx"]) and asserts the loader points to the user file and does not contain import("next/error").

2. Path convention is correct. findFileWithExts builds its result with path.join (pathslash in source), so the emitted import(...) specifier is a canonical forward-slash id, matching how the other pageRoutes loaders serialize r.filePath. No slash-convention divergence.

3. New e2e coverage. custom-error-navigation.browser.spec.ts builds a real prod fixture with a custom pages/_error.tsx (with getInitialProps), clicks a link to /missing, and asserts the user's data-testid="custom-error" renders with the correct statusCode (404) — locking the client-navigation path end-to-end. The fixture follows the established sibling-test pattern (default-error.browser.spec.ts).

Verified, no action needed (re-confirmed on this head)

  • Server/client now resolve _error via the identical findFileWithExts(pagesDir, "_error", fileMatcher) call — no residual divergence between the two entries.
  • Prod renderProps.pageProps !== undefined guard (pages-page-handler.ts:663-670) still preserves a custom _app envelope without pageProps while injecting statusCode for the framework/default error page.
  • statusCode flow: res.statusCode set before getInitialProps (createPageReqRes); final HTTP status from the explicit statusCode arg.
  • next/error port fidelity, concurrency isolation (route-miss caller wrapped in its own runWithRequestContext), and err.statusCode! upstream-matching unsoundness — all unchanged and still correct.

Remaining (nit — non-blocking, unchanged)

errorProps/initialErrorProps in dev are still computed eagerly and discarded on the custom-_app-with-getInitialProps branch. Behavior is correct; the eager compute is a cheap, harmless redundancy. Not worth a dedicated follow-up.

No correctness blocker. The medium bug from the prior pass is resolved with matching unit + e2e coverage, and dev/prod/server/client now agree on the _error component. This looks ready.

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I re-reviewed the exact head (67d074f1), which adds one follow-up commit on top of the previously-reviewed 8876103. The one medium-severity correctness bug from the last review pass — custom pages/_error being ignored by the production client loader map — is now fixed, with matching unit + e2e coverage, and the follow-up introduces no new issues.

Verification (all green):

  • vp check on all 3 changed files — clean
  • vp test run tests/entry-templates.test.ts tests/pages-page-handler.test.ts tests/shims.test.ts → 1,266 passed

What the fix does: pages-client-entry.ts:106-110 replaces the never-firing pageRoutes.some(r => r.pattern === "/_error") guard (_error is a reserved name excluded from pageRoutes, so it was always false) with a real findFileWithExts(pagesDir, "_error", fileMatcher) resolution — the exact same call the server entry uses. The client loader map now points to the user's custom _error file when present, so client-side navigation to /_error loads the app's own component instead of the framework next/error. This restores server/client and dev/prod parity, and is locked in by a new prod e2e test plus a companion entry-templates assertion.

Remaining: one unchanged nit (eager dev errorProps compute discarded on the custom-_app branch — cheap, harmless, non-blocking).

No correctness blocker. This looks ready.

github run

Comment thread packages/vinext/src/entries/pages-client-entry.ts Dismissed
createValidFileMatcher(nextConfig.pageExtensions),
);

expect(clientCode).toContain(`"/_error": () => import(${JSON.stringify(errorFilePath)})`);
@james-elicx

Copy link
Copy Markdown
Member Author

Independent Codex review — NO FINDINGS

Reviewed exact head 67d074f18fc22d849a2e6b44f9a8f5fba06f6ca4. Scope: custom Pages _error client-loader parity across supported extensions, route exclusion, dev/production error behavior, generated-entry correctness, and preservation of intentionally missing pageProps. Targeted Next.js v16.3.0-canary.80 oracle: test/e2e/no-page-props/no-page-props.test.ts5 passed, 0 failed.

…ge-props-28938793088

# Conflicts:
#	packages/vinext/src/server/pages-page-handler.ts
#	packages/vinext/src/shims/next-shims.d.ts
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

Comment thread packages/vinext/src/server/pages-dev-hydration.ts Dismissed
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Bonk review — PR #2568

Re-reviewed the exact head (79074bf, refactor(pages): share dev hydration entry), the one commit landed on top of the last-reviewed 67d074f1 that no prior Bonk pass covered. It extracts the two inline dev hydration scripts (main-page path + renderErrorPage path) into a single shared createPagesDevHydrationScript helper in server/pages-dev-hydration.ts. I diffed the extracted helper line-by-line against both original inline scripts to check for behavioral drift, ran the targeted suites, and reviewed the CodeQL alert on the new file.

Verification (all green)

  • vp check packages/vinext/src/server/pages-dev-hydration.ts packages/vinext/src/server/dev-server.ts — clean (format + lint + type)
  • vp test run tests/pages-page-handler.test.ts tests/shims.test.ts tests/entry-templates.test.ts tests/pages-dev-hydration.test.ts → 1,279 passed
  • vp test run tests/pages-router.test.ts -t "falls back to pages/_error for route misses" → 1 passed

Refactor fidelity — verified faithful

I traced every option branch against the pre-refactor inline scripts:

Behavior Main page (replaceFallbackRoute: true) Error page (forceRouterReady/normalizePageProps: false/setPagePatterns: true)
_initializePagesRouterReadyFromNextData (nextData) (nextData, true)
pageProps normalization rawPageProps && typeof === "object" ? : {} rawPageProps ?? {} ✓ (matches old props.pageProps ?? {})
app-branch router Router { ...Router, isReady: true } ✓ (matches old initialRouter)
__VINEXT_PAGE_PATTERNS__ omitted ✓ set ✓
isFallback replace tail present ✓ omitted ✓

All branches match the originals. The pages-dev-hydration.test.ts unit test locks in both shapes plus the escaping behavior.

Security improvement (worth calling out)

The extraction changes module-specifier serialization from raw string interpolation (import("${pageModuleSource}")) to import(${JSON.stringify(...)}). This fixes the pre-existing bad-code-sanitization pattern the inline scripts had — a path containing a " or newline previously would have produced broken/injectable script text. The new test at pages-dev-hydration.test.ts:42-51 asserts quotes and newlines in specifiers are now safely escaped.

The remaining CodeQL alert on pages-dev-hydration.ts:70 is the same accepted false-positive class already suppressed with lgtm[js/bad-code-sanitization] at pages-client-entry.ts:103 — a trusted filesystem path serialized via JSON.stringify. Not a real vulnerability. Nit: for consistency with the sibling entry, consider adding the same // lgtm[js/bad-code-sanitization] suppression comment above line 68/70 so the alert doesn't recur on future scans.

Minor behavioral deltas on the error path (both benign)

The unified helper is now used for the error path, which slightly widens two behaviors vs. the old inline error script — neither is a regression:

  1. Locale globals now set on the error path. The unified script always runs window.__VINEXT_LOCALE__ = nextData.locale (etc.). The error __NEXT_DATA__ (dev-server.ts:2249-2257) omits locale/locales/defaultLocale, so these resolve to undefined — the same effective state as before (previously untouched). No behavior change.
  2. nextDataElement?.textContent null-guard added on the error path. The old error script did an unconditional JSON.parse(nextDataElement.textContent). The new guarded form is strictly safer; the error __NEXT_DATA__ is always emitted, so the guarded branch always runs.

Verified, no action needed (carried from prior passes, re-confirmed on this head)

  • Custom pages/_error client-loader parity (prior finding Virtual module imports break esbuild dependency optimization when vinext is installed from npm #1) remains fixed at pages-client-entry.ts:106-110.
  • Prod renderProps.pageProps !== undefined guard still preserves the no-pageProps envelope.
  • statusCode flow, concurrency isolation (route-miss caller in its own runWithRequestContext + finally { setSSRContext(null) }), and err.statusCode! upstream-matching unsoundness — all unchanged and correct.

Remaining (nit — non-blocking, unchanged)

Dev errorProps/initialErrorProps are still computed eagerly and discarded on the custom-_app-with-getInitialProps branch. Cheap, harmless redundancy.

No correctness blocker. The refactor is a faithful, well-tested extraction that also hardens module-specifier escaping. This looks ready.

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2568.

Summary

I reviewed the exact head (79074bf, refactor(pages): share dev hydration entry) — the one commit landed since the last-reviewed 67d074f1 that no prior Bonk pass had covered. It extracts the two inline dev hydration scripts into a shared createPagesDevHydrationScript helper (server/pages-dev-hydration.ts).

The refactor is a faithful, well-tested extraction — no correctness blocker.

Key findings:

  • Fidelity verified. I diffed the helper's option branches (main-page vs error-page) line-by-line against both original inline scripts: router init, pageProps normalization, app-branch router, __VINEXT_PAGE_PATTERNS__, and the fallback-replace tail all match exactly.
  • Security improvement. The extraction switches module-specifier serialization from raw interpolation (import("${x}")) to import(${JSON.stringify(x)}), fixing the pre-existing bad-code-sanitization pattern. The new unit test asserts quotes/newlines are now escaped.
  • CodeQL alert on pages-dev-hydration.ts:70 is the same accepted false-positive class already suppressed at pages-client-entry.ts:103 (trusted FS path via JSON.stringify). Nit: add a matching lgtm suppression for consistency.
  • Two benign error-path deltas (locale globals now set from __NEXT_DATA__ that omits them → resolve undefined; safer null-guard on nextDataElement) — neither is a regression.

Verification: vp check clean on both changed source files; 1,279 targeted tests + the pages-router fallback test all green.

github run

@james-elicx james-elicx merged commit 6734371 into main Jul 9, 2026
66 checks passed
@james-elicx james-elicx deleted the codex/fix-pages-no-page-props-28938793088 branch July 9, 2026 15:23
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