Skip to content

fix(pages): preserve app props for GSSP requests#1996

Merged
james-elicx merged 19 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/gssp-upstream-block
Jun 14, 2026
Merged

fix(pages): preserve app props for GSSP requests#1996
james-elicx merged 19 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/gssp-upstream-block

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Summary

Ports the scoped Pages Router getServerSideProps behavior from upstream Next.js and fixes vinext's Pages render/data pipeline to preserve full _app.getInitialProps props for GSSP/GSP requests.

This PR makes Pages Router handling match the upstream contract for:

  • _next/data JSON returning the full props object (pageProps plus app-level props) when _app.getInitialProps is present
  • _app.getInitialProps(ctx) receiving the original req.url, route pathname, merged query, and visible asPath
  • getServerSideProps({ resolvedUrl }) using the resolved page pathname plus the original request search
  • config rewrites like /blog-:param -> /blog/post-3 carrying unused source params into the target query

Upstream references

Verification

  • vp test run --project integration tests/pages-router.test.ts
  • vp test run tests/shims.test.ts -t "matchRewrite with external URLs"
  • vp check
  • Upstream deploy harness: 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/getserversideprops/test/index.test.ts
    • Scoped server/data request block now passes, including the dynamic rewrite-param case.
    • The full upstream file still has unrelated existing failures around client navigation/router mounting, #__NEXT_DATA__ script parsing, server data request dedupe, and invalid JSON GSSP fallback behavior.

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1996
npm i https://pkg.pr.new/vinext@1996

commit: 25556de

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/gssp-upstream-block branch 3 times, most recently from a4cd836 to a5b8203 Compare June 13, 2026 10:29
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/gssp-upstream-block branch from a5b8203 to 5382aff Compare June 13, 2026 10:53
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review June 13, 2026 11:04
NathanDrake2406 and others added 4 commits June 14, 2026 02:45
…eneration

Stale ISR background regeneration called getStaticProps directly and rendered
HTML and __NEXT_DATA__ with only the page's pageProps, dropping app-level
props from _app.getInitialProps. The foreground path already loads
_app.getInitialProps first and merges its props, but the background
regeneration path skipped that step.

Extract a shared loadPagesAppInitialRenderProps helper from the production
foreground path and reuse it during background regeneration in both
pages-page-data.ts and dev-server.ts. This ensures regenerated pages carry the
same full props envelope as the original render.

Add a regression test in tests/pages-page-data.test.ts.
The foreground render path ran _app.getInitialProps before consulting the
ISR cache, so fresh and stale cache hits executed userland App data code
even though the cached HTML was returned verbatim. That wasted work on the
hot path and ran side-effecting App code whose result the cached response
never reflected.

The eager App initial-props load happened up front, ahead of the HIT and
STALE short-circuits. Defer it until the request will actually render:
load it lazily inside the getServerSideProps / getStaticProps /
getInitialProps branches, after the cache short-circuits return.
Stale-while-revalidate already re-runs it inside the regeneration closure,
so regenerated HTML still carries app-level props. The dev SSR handler and
the production page-data resolver are updated in lockstep to preserve
dev/prod parity.

Tests cover the fresh-HIT skip and the stale foreground-skip vs
regeneration-run split at the page-data resolver boundary.
… tested helper

The dev SSR handler ran _app.getInitialProps inside an inline closure that
mutated captured pageProps/renderProps and read res directly, so its lazy
load-only-on-render behavior could only be covered by a heavyweight
integration test that vi.doMock'd the isr-cache and file-matcher modules —
a mock cascade with little signal.

Move the decision into loadDevAppInitialProps in pages-get-initial-props.ts.
It takes the App component, req/res, and an injected AppTree builder (keeping
React out of the module) and returns a discriminated result —
skip / response-sent / render — that the caller applies. The dev-server
closure is now thin glue: build the React tree, delegate, apply the result.

Adds focused unit tests that drive the helper through its real boundary
(genuine getInitialProps functions, plain req/res inputs) — no module mocks:
the skip path, App/page prop merging, pageProps normalization, the
response-sent short-circuit, and the router/ctx wiring passed to userland.
@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

Solid, well-tested PR. The core change — threading the full Pages props envelope (pageProps plus _app.getInitialProps app-level props and __N_SSP) through the data/render/hydration pipeline — is implemented consistently across the dev server (dev-server.ts), the prod resolver (pages-page-data.ts), and the client router shim (shims/router.ts). I verified dev/prod parity holds: both dev-server.ts and the loadPagesAppInitialRenderProps helper run _app.getInitialProps lazily (only on real renders, never on a cache HIT/STALE that serves cached HTML), and the Cloudflare/prod worker path inherits the behavior via createPagesPageHandler (which defaults originalUrl from request.url). The generatedFromDataRequest fallback-data flow is mirrored in both servers, and the new helper module (pages-get-initial-props.ts) is cleanly factored out and directly unit-tested per the repo's entry-thinning guidance.

I ran the targeted suites locally — all green:

  • tests/pages-page-data.test.ts (43) + tests/pages-get-initial-props.test.ts (5)
  • tests/shims.test.ts -t "matchRewrite with external URLs" (10)
  • tests/entry-templates.test.ts (26)
  • tests/pages-router.test.ts GSSP + fallback-hydration cases
  • vp check on all changed source files (no fmt/lint/type errors)

The config-matchers.ts rewrite-param-to-query logic faithfully mirrors Next.js's prepare-destination.ts (unused source params appended to query, params consumed by path/host/hash excluded), with good test coverage including the hash-consumed case.

Comments below are minor — none are blocking.

let element: ReactElement;
if (AppComponent) {
element = React.createElement(AppComponent, {
...props,

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.

Spreading ...props onto the App element forwards the internal envelope markers (__N_SSP, and on the gSSP-redirect path __N_REDIRECT/__N_REDIRECT_STATUS live in pageProps so those are fine, but __N_SSP/__N_SSG are top-level in props) as actual props to the user's <App>. The server-side render does the identical ...renderProps spread (dev-server.ts, pages-page-handler.ts), so client and server are self-consistent and hydration won't mismatch — but Next.js does not pass __N_SSP to <App>. It's a harmless extra prop today; worth a follow-up to strip the __N_* keys before constructing the element if you want exact parity. Not blocking.


if (shouldPersistFallbackData) {
const revalidateSeconds = isrRevalidateSeconds ?? 31_536_000;
await options.isrSet(

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.

This isrSet for the persisted fallback data omits tags and expireSeconds, whereas the other isrSet calls in this file pass undefined, options.expireSeconds. The generated entry will use the backend's default expiry instead of the configured expireSeconds. Likely intentional (the entry is short-lived and regenerated), but flagging for consistency:

Suggested change
await options.isrSet(
await options.isrSet(
cacheKey,
{
kind: "PAGES",
html: "",
pageData: renderProps,
generatedFromDataRequest: true,
headers: undefined,
status: undefined,
},
revalidateSeconds,
undefined,
options.expireSeconds,
);

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written by writeGsspAppInitialPropsContextFixture but never requested by any test (only /blog/[post] and /rewrite-target are exercised). It looks like it was meant to cover a getServerSideProps({ query })/asPath-with-query case. Either add an assertion that fetches /something?... or drop the fixture to avoid dead test setup.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every route (not just GSSP/data requests). For the common case originalUrl defaults to url so this is a no-op, but it's a side effect on shared request state late in the handler. Consider scoping the mutation to the data-fetching contexts that actually need it (gSSP req.url), or add a brief comment that this is the intended Next.js req.url contract for data-fetching methods, so a future reader doesn't mistake it for an accidental reassignment.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 4 inline comments (all minor, none blocking).

What the PR does well:

  • Threads the full Pages props envelope (pageProps + _app.getInitialProps app-level props + __N_SSP) through the data/render/hydration pipeline consistently across dev server, prod resolver, and the client router shim.
  • Maintains dev/prod parity — both run _app.getInitialProps lazily (never on a cache HIT/STALE), and the Cloudflare worker path inherits behavior via createPagesPageHandler's originalUrl defaulting.
  • Cleanly extracts loadDevAppInitialProps/loadPagesAppInitialRenderProps into a directly-unit-tested helper module, following the repo's entry-thinning guidance.
  • config-matchers.ts rewrite-param-to-query logic faithfully mirrors Next.js's prepare-destination.ts.

Verification I ran locally (all green):

  • tests/pages-page-data.test.ts (43) + tests/pages-get-initial-props.test.ts (5)
  • tests/shims.test.ts rewrite tests (10)
  • tests/entry-templates.test.ts (26)
  • tests/pages-router.test.ts GSSP + fallback-hydration cases
  • vp check on all changed source files — no fmt/lint/type errors

Minor findings flagged:

  1. ...props spread forwards __N_SSP as a real prop to user <App> (self-consistent client/server, but differs from Next.js — follow-up parity).
  2. Persisted fallback-data isrSet omits expireSeconds (consistency, suggested fix included).
  3. something.jsx test fixture is written but never requested — dead test setup.
  4. req.url = originalUrl mutates request state unconditionally — suggest scoping or documenting the intent.

github run

@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

The core change — threading the full Pages props envelope (pageProps + _app.getInitialProps app-level props + __N_SSP) through data/render/hydration — is implemented thoroughly and well-tested. The config-matchers.ts rewrite-param-to-query logic faithfully mirrors Next.js's prepare-destination.ts, and the helper extraction (loadDevAppInitialProps / loadPagesAppInitialRenderProps) follows the repo's entry-thinning guidance.

I found one dev/prod parity bug worth fixing in this PR, plus a couple of minor items. AGENTS.md explicitly calls out keeping dev-server and prod-resolver request handling in sync, so I'd treat the parity item as blocking-ish rather than a follow-up.

Dev/prod parity: stale-ISR regen stores the wrong cache pageData in dev

In the prod resolver (pages-page-data.ts:872), the stale-while-revalidate background regeneration writes the full envelope: buildPagesCacheValue(freshHtml, freshRenderProps, options.statusCode). The dev server's equivalent path (dev-server.ts:1231) writes only freshPageProps. Since the regenerated HTML embeds props: freshRenderProps in __NEXT_DATA__ (dev-server.ts:1211), but the cache pageData only holds freshPageProps, a later _next/data HIT served from this regenerated entry will return just pageProps and drop the app-level props produced by _app.getInitialProps — exactly the regression this PR is fixing, but reintroduced for the dev background-regen path. There's no dev test covering a _next/data HIT after a dev stale regen. Suggested fix inline.

Verified locally (all green): tests/pages-page-data.test.ts, tests/pages-get-initial-props.test.ts, tests/shims.test.ts rewrite tests, tests/entry-templates.test.ts.

Comments below are otherwise minor.

await isrSet(
cacheKey,
buildPagesCacheValue(freshHtml, freshProps),
buildPagesCacheValue(freshHtml, freshPageProps),

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.

Dev/prod parity bug. The prod resolver writes the full props envelope here (pages-page-data.ts:872 passes freshRenderProps), but dev passes only freshPageProps. The regenerated HTML above (line 1211) already embeds props: freshRenderProps in __NEXT_DATA__, so the cache pageData is now inconsistent with the HTML — and a subsequent _next/data HIT served from this entry returns only pageProps, dropping the _app.getInitialProps app-level props this PR is meant to preserve.

Suggested change
buildPagesCacheValue(freshHtml, freshPageProps),
await isrSet(
cacheKey,
buildPagesCacheValue(freshHtml, freshRenderProps),
revalidate,
);

Worth adding a dev test that does a stale regen then a _next/data fetch and asserts app-level props survive, mirroring the prod-side coverage in pages-page-data.test.ts.


if (shouldPersistFallbackData) {
const revalidateSeconds = isrRevalidateSeconds ?? 31_536_000;
await options.isrSet(

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.

The persisted fallback-data isrSet omits expireSeconds, whereas the regen isrSet above (line 870) passes undefined, options.expireSeconds. This entry will fall back to the backend default expiry instead of the configured one. Likely intentional (short-lived, regenerated on next miss), but for consistency:

Suggested change
await options.isrSet(
await options.isrSet(
cacheKey,
{
kind: "PAGES",
html: "",
pageData: renderProps,
generatedFromDataRequest: true,
headers: undefined,
status: undefined,
},
revalidateSeconds,
undefined,
options.expireSeconds,
);

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written by writeGsspAppInitialPropsContextFixture but never fetched by any test (only /blog/[post], /blog-*, and /rewrite-source/* are exercised). It looks intended to cover a getServerSideProps({ query }) / asPath-with-query case. Either add an assertion that fetches /something?... or drop the fixture to avoid dead test setup.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every route, not just GSSP/data requests. For the common case originalUrl defaults to url, so it's a no-op, but it's a side effect on shared request state. Consider scoping the assignment to the data-fetching contexts that actually consume req.url, or add a brief comment noting this implements the Next.js req.url contract for data-fetching methods so a future reader doesn't read it as an accidental reassignment.

let element: ReactElement;
if (AppComponent) {
element = React.createElement(AppComponent, {
...props,

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.

Spreading ...props onto the App element forwards top-level envelope markers like __N_SSP/__N_SSG as actual props to the user's <App>. The server render does the identical ...renderProps spread (dev-server.ts, pages-page-handler.ts), so client/server are self-consistent and hydration won't mismatch — but Next.js does not pass __N_SSP to <App>. Harmless today; worth a follow-up to strip __N_* keys before constructing the element for exact parity. Not blocking.

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 5 inline comments.

Main finding (dev/prod parity bug — worth fixing in this PR):

The stale-while-revalidate background regeneration in the dev server (dev-server.ts:1231) writes buildPagesCacheValue(freshHtml, freshPageProps) — only the page props. The prod resolver (pages-page-data.ts:872) writes the full envelope freshRenderProps. Since the regenerated dev HTML already embeds props: freshRenderProps in __NEXT_DATA__, the cached pageData is now inconsistent with the HTML, and a later _next/data HIT served from that regenerated entry returns only pageProps — dropping the _app.getInitialProps app-level props this PR exists to preserve. AGENTS.md explicitly calls out keeping dev/prod request handling in sync, and there's no dev test covering a _next/data HIT after a dev stale regen. Included a suggested fix and a test recommendation.

Minor items flagged:

  1. Fallback-data isrSet (pages-page-data.ts:973) omits expireSeconds (inconsistent with the regen isrSet 100 lines up) — suggestion included.
  2. something.jsx fixture is written but never fetched — dead test setup.
  3. req.url = originalUrl (dev-server.ts:554) mutates shared request state unconditionally — suggest scoping or documenting.
  4. ...props spread forwards __N_SSP/__N_SSG to user <App> — self-consistent but diverges from Next.js; follow-up parity.

Verification I ran locally: confirmed the dev/prod divergence by reading both buildPagesCacheValue call sites and the generatedFromDataRequest HIT path, validated isUnknownRecord/wrapWithRouterContext signatures, and confirmed the something.jsx fixture is unreferenced.

The first review submission hit a 422 because I'd put a wrong line number on the pages-page-data.ts comment (1534 vs the actual 973); corrected and resubmitted successfully.

github run

@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

Thorough, well-tested change. The full Pages props envelope (pageProps + _app.getInitialProps app-level props + __N_SSP) is threaded consistently through data/render/hydration across the dev server, the prod resolver, the prod page handler, and the client router shim. I verified the key correctness properties:

  • Dev/prod parity on app-GIP timing holds. Both loadDevAppInitialProps and loadPagesAppInitialRenderProps run _app.getInitialProps lazily — only on real renders, never on a fresh/stale ISR HIT that serves cached HTML (the HIT short-circuits in pages-page-data.ts are gated on !isDataReq, so _next/data requests always regenerate the full envelope rather than reading stale pageData). The Cloudflare worker path inherits the behavior through createPagesPageHandler's originalUrl defaulting.
  • The two parity items prior reviews flagged are already fixed in the current head. The dev stale-regen now writes buildPagesCacheValue(freshHtml, freshRenderProps) (dev-server.ts:1239), matching prod (pages-page-data.ts:872), and the prod fallback-persist isrSet now passes options.expireSeconds (pages-page-data.ts:985).
  • config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts (unused source params appended to query; params consumed by path/host/hash excluded). The new /[^/]:[\w-]+/ regex-mode trigger is scoped to source path patterns, so schemes like https:// are not a concern.
  • The _h: 1 query-updating navigation (fallback Router.replace) correctly suppresses routeChangeStart/routeChangeComplete and bypasses the hash-only-change early return so the data fetch still happens, matching Next.js's query-update mechanism.

Verified locally (all green): tests/pages-page-data.test.ts (43) + tests/pages-get-initial-props.test.ts (5), and tests/shims.test.ts rewrite tests (28).

One minor cleanup item below; nothing blocking.

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written here but never requested by any test. The GSSP context test (line 1517) only exercises /blog/[post], /blog-post-*, and /rewrite-source/*/something is never fetched, so its getServerSideProps({ query }) / asPath-with-query assertions never run. Either add a fetch + assertions for /something?... (it looks intended to cover the query-merge / initial-query case) or drop the fixture to avoid dead test setup. This was flagged on two prior review passes and still persists.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every matched route, not just GSSP/data requests. For the common case originalUrl defaults to url, so it is a no-op, but it is a side effect on shared request state read later at line 1090 (regenReq = { url: req.url, ... }). It is in fact intentional — it implements the Next.js req.url contract for data-fetching methods (the data test asserts appProps.url === "/_next/data/.../blog/post-1.json?hello=world"). Worth a one-line comment noting that, so a future reader does not mistake it for an accidental reassignment.

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 2 inline comments.

What I verified (and where this review differs from prior passes):

Two of the items that earlier review passes flagged as bugs have since been fixed in the current head — I confirmed this by reading the actual source rather than relying on the prior diffs:

  1. The dev stale-ISR regeneration now writes the full envelope buildPagesCacheValue(freshHtml, freshRenderProps) (dev-server.ts:1239), matching prod — the parity bug is resolved.
  2. The prod fallback-persist isrSet now passes options.expireSeconds (pages-page-data.ts:985) — the consistency item is resolved.

I also independently chased down a potential new bug — the initial ISR write storing only pageData: pageProps in both dev (dev-server.ts:1785) and prod (pages-page-response.ts:616) — and confirmed it is not a regression: the fresh/stale cache-HIT short-circuits are gated on !isDataReq, so _next/data requests always bypass the HIT and regenerate the full envelope. The cached pageData is never read back as the data response for regular gSP pages, and the only path that does read pageData back (generatedFromDataRequest entries) correctly stores renderProps.

Verified locally (all green): tests/pages-page-data.test.ts (43), tests/pages-get-initial-props.test.ts (5), tests/shims.test.ts rewrite tests (28).

Remaining minor items (nothing blocking):

  1. something.jsx test fixture is written but never fetched — dead test setup (persists across 3 review passes now).
  2. req.url = originalUrl (dev-server.ts:554) mutates request state unconditionally — it is intentional (implements the Next.js req.url contract), but deserves a one-line comment.

The core implementation is sound, dev/prod parity holds, and the config-matchers.ts rewrite-param logic faithfully mirrors Next.js.

github run

@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

Thorough, well-tested change. I independently verified the key correctness properties rather than relying on the prior review passes:

  • Dev/prod parity holds across all four request paths. Both the dev server (via pipelineDepsrunPagesRequest) and the prod/worker paths share pages-request-pipeline.ts, so the new x-nextjs-rewrite header on data responses (pages-request-pipeline.ts:368) is emitted identically in dev, prod, and Workers — the client-navigation "hydrate middleware rewrite to fallback page" feature works the same everywhere. The dev server has no separate x-nextjs-rewrite codepath to drift out of sync.
  • App-GIP timing is lazy in both dev and prod. The fresh/stale ISR HIT short-circuits in pages-page-data.ts are gated on !isDataReq and the generatedFromDataRequest foreground branch skips loadForegroundAppInitialRenderProps() entirely, so _app.getInitialProps never runs on a cache hit. The dedicated tests (does not run _app.getInitialProps on a fresh ISR cache HIT, only runs ... in the stale ISR regeneration path) lock this in.
  • The two parity items earlier passes flagged are confirmed fixed in the current head: the dev stale-regen and prod stale-regen both write the full envelope via buildPagesCacheValue(freshHtml, freshRenderProps), and the foreground fallback-persist isrSet now passes options.expireSeconds (pages-page-data.ts:985).
  • config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts: unused source params are appended to query, params consumed by the destination path/host/hash are excluded, and nextInternalLocale is filtered. The getDestinationPathAndHost scheme-stripping correctly avoids treating https:// as a path-consumed param.
  • The redirect data envelope now threads __N_SSP and app-level props as siblings of pageProps while keeping __N_REDIRECT/__N_REDIRECT_STATUS inside pageProps, matching Next.js's pageProps.__N_REDIRECT contract.

Two small cleanup items below; nothing blocking. The __N_SSG flag for gSP pages is still never set (Next.js sets it) — that and the __N_*-leaking-to-<App> parity item from prior passes are reasonable follow-ups, not blockers, since client and server are self-consistent.

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written here but never requested by any test. The GSSP context test (passes original req.url, query, asPath, and resolvedUrl through _app.getInitialProps) only fetches /blog/post-1, the /blog-post-* rewrites, and /rewrite-source/foo/something is never fetched, so its getServerSideProps({ query }) / initial-query / asPath-with-query assertions never run. Either add a fetch + assertions for /something?... (it looks intended to cover the query-merge case) or drop the fixture to avoid dead test setup. This has persisted across all three prior review passes.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every matched route, not just GSSP/data requests. For the common case originalUrl defaults to url so it's a no-op, but it is a side effect on shared request state that is read again later (e.g. the regen request snapshot at regenReq = { url: req.url, ... }). It is intentional — it implements the Next.js req.url contract for data-fetching methods (the data test asserts appProps.url === "/_next/data/.../blog/post-1.json?hello=world"). Worth a one-line comment so a future reader doesn't mistake it for an accidental reassignment.

Suggested change
req.url = originalUrl;
// Implements the Next.js `req.url` contract: data-fetching methods
// (gSSP/gSP `_app.getInitialProps`) observe the original request URL.
// Defaults to `url` for non-data requests, so this is a no-op there.
req.url = originalUrl;

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 2 inline comments (nothing blocking).

What I independently verified (rather than trusting prior passes):

  1. Dev/prod/Workers parity for the new x-nextjs-rewrite data header. I traced that the dev server invokes the same runPagesRequest pipeline (packages/vinext/src/index.ts:3644 pipelineDeps) as prod and the worker, so the header set at pages-request-pipeline.ts:368 is emitted identically everywhere — there's no separate dev codepath that could drift. The client-navigation "hydrate middleware rewrite to fallback page" feature is therefore covered in all runtimes.

  2. App-GIP runs lazily — confirmed the fresh/stale ISR HIT short-circuits are gated on !isDataReq and the generatedFromDataRequest foreground branch skips loadForegroundAppInitialRenderProps(), so _app.getInitialProps never runs on a cache hit. Locked in by two dedicated tests.

  3. The two parity bugs from earlier passes are fixed in the current head: dev + prod stale-regen both write the full envelope (buildPagesCacheValue(freshHtml, freshRenderProps)), and the foreground fallback-persist isrSet passes options.expireSeconds (pages-page-data.ts:985).

  4. config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts, including the scheme-stripping in getDestinationPathAndHost that prevents https:// from being mistaken for a path-consumed param.

Minor items flagged (non-blocking):

  • something.jsx fixture written but never fetched — dead test setup (persists across all prior passes).
  • req.url = originalUrl (dev-server.ts:554) unconditional mutation — intentional per the Next.js req.url contract; suggested a clarifying comment.
  • Noted __N_SSG is never set for gSP pages (Next.js sets it) as a reasonable follow-up, not a blocker.

The core implementation is sound and well-tested.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

Thorough, well-tested change. The full Pages props envelope (pageProps + _app.getInitialProps app-level props + __N_SSP) is threaded consistently across the dev server, prod resolver, prod page handler, and client router shim. I independently verified the key correctness properties this pass:

  • The beforeFiles chained-rewrite change is correct and matches Next.js. The PR changes pages-request-pipeline.ts Step 9 from a single first-match rewrite to a sequential loop that re-feeds each rewritten pathname into the next beforeFiles rule. I confirmed against Next.js resolve-routes.ts: when a non-external beforeFiles rewrite matches, Next sets didRewrite = true, updates parsedUrl.pathname, and does not return — it continues the route loop so the next beforeFiles rule matches against the already-rewritten path. The new test ("applies every matching beforeFiles rewrite in sequence") encodes correct behavior.
  • config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts — unused source params appended to query, params consumed by destination path/host/hash excluded, nextInternalLocale filtered, scheme-stripping in getDestinationPathAndHost prevents https:// being misread as a path-consumed param.
  • App-GIP runs lazily — fresh/stale ISR HIT short-circuits are gated on !isDataReq and the generatedFromDataRequest foreground branch skips app GIP, locked in by dedicated tests. Dev stale-regen and prod stale-regen both write the full envelope. The two parity bugs from earlier passes are confirmed fixed.
  • mergeRewriteQuery correctly preserves the original query (incl. RSC _rsc) on App Router rewrites, so the url.search = "" branch in applyInternalRewriteDestination only fires when both original and rewrite are query-less.

Verified locally (all green): pages-page-data.test.ts (43), pages-get-initial-props.test.ts (5), pages-request-pipeline.test.ts (44), entry-templates.test.ts (26), shims.test.ts rewrite tests (28), app-router-next-config-dev.test.ts (16), plus vp check on the changed source files.

One new gap worth addressing plus two persistent minor items below. Nothing blocking.

);
if (beforeFilesRewrite instanceof Response) return beforeFilesRewrite;
if (beforeFilesRewrite) cleanPathname = beforeFilesRewrite;
if (beforeFilesRewrite) cleanPathname = applyInternalRewriteDestination(beforeFilesRewrite, url);

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.

New App Router behavior with no App Router test. This PR routes the App Router's beforeFiles/afterFiles/fallback rewrites through the new applyInternalRewriteDestination, which (via config-matchers.ts) appends unused source params to the destination query and merges them into url.search. That's a real cross-cutting change to App Router request handling — e.g. a config rewrite { source: "/blog-:param", destination: "/blog/post-3" } now exposes ?param=... to the App Router page's searchParams, and url.search mutation feeds downstream searchParams.

The new tests cover this for the Pages Router (pages-request-pipeline.test.ts, pages-router.test.ts) and the matcher itself (shims.test.ts), but there is no App Router test asserting that a rewrite's unused source params reach searchParams (or that the original query/_rsc survives). app-router-next-config-dev.test.ts already has a rewrite fixture harness — please add a case there so this App Router code path is covered and protected against regression.

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written here but never requested by any test — the GSSP context test at line 1517 only fetches /blog/post-1, /blog-post-2, /blog-post-3, and /rewrite-source/foo. Its getServerSideProps({ query }) / initial-query / asPath-with-query assertions never run. Either add a /something?... fetch + assertions (it looks intended to cover the query-merge case) or drop the fixture. This has persisted across all prior review passes.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every matched route, not just GSSP/data requests. For the common case originalUrl defaults to url so it's a no-op, but it is a side effect on shared request state read again later (the regen snapshot regenReq = { url: req.url, ... }). It is intentional — it implements the Next.js req.url contract for data-fetching methods (the data test asserts appProps.url === "/_next/data/.../blog/post-1.json?hello=world"). Worth a one-line comment so a future reader doesn't mistake it for an accidental reassignment.

Suggested change
req.url = originalUrl;
// Implements the Next.js `req.url` contract: data-fetching methods
// (gSSP/gSP `_app.getInitialProps`) observe the original request URL.
// Defaults to `url` for non-data requests, so this is a no-op there.
req.url = originalUrl;

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 3 inline comments.

What I independently verified this pass (focusing on things prior passes hadn't):

  1. The beforeFiles chained-rewrite change is correct and matches Next.js. The PR changes Step 9 in pages-request-pipeline.ts from a single first-match rewrite to a loop that re-feeds each rewritten pathname into the next beforeFiles rule. I fetched and read Next.js resolve-routes.ts to confirm: on a non-external beforeFiles match Next sets didRewrite = true, updates parsedUrl.pathname, and continues the route loop (no return), so the next rule sees the rewritten path. The new test encodes correct behavior — not a bug.

  2. New App Router gap (main finding). The PR routes App Router beforeFiles/afterFiles/fallback rewrites through the new applyInternalRewriteDestination, which appends unused rewrite source params to the destination query and mutates url.search (feeding App Router searchParams). This is a real cross-cutting App Router behavioral change with no App Router test — coverage exists only for Pages Router and the matcher. I confirmed mergeRewriteQuery preserves the original query/_rsc, so it's behaviorally sound, but recommended adding a case to the existing app-router-next-config-dev.test.ts harness.

  3. Confirmed the two parity bugs from earlier passes are fixed, app-GIP runs lazily, and config-matchers.ts mirrors Next.js prepare-destination.ts.

Verified locally (all green): pages-page-data (43), pages-get-initial-props (5), pages-request-pipeline (44), entry-templates (26), shims rewrite (28), app-router-next-config-dev (16), vp check.

Minor persistent items flagged: the unused something.jsx fixture (dead test setup) and the undocumented unconditional req.url = originalUrl mutation (intentional, suggested a clarifying comment).

Nothing blocking.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

5 similar comments
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/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.

Review: preserve app props for GSSP requests

Thorough, well-tested change. The full Pages props envelope (pageProps + _app.getInitialProps app-level props + __N_SSP) is threaded consistently across the dev server, prod resolver, prod page handler, and client router shim. The two parity bugs earlier passes flagged are confirmed fixed in the current head (dev + prod stale-regen both write the full envelope; the foreground fallback-persist isrSet passes options.expireSeconds), and config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts.

Verified locally (all green): pages-page-data (43), pages-get-initial-props (5), pages-request-pipeline (44).

One new internal-consistency item and one coverage gap below, plus the two persistent minor items. Nothing blocking.

Foreground ISR write stores page-only pageData while the regen path stores the full envelope

The foreground ISR cache writes store pageData: pageProps (page-only) in both dev (dev-server.ts:1786, buildPagesCacheValue(isrHtml, pageProps)) and prod (pages-page-response.ts:616, pageData: options.pageProps), while the stale-regen paths now store the full freshRenderProps envelope (dev-server.ts:1239, pages-page-data.ts:869) — and the cached HTML embeds the full renderProps in __NEXT_DATA__ in all cases. So the pageData field is inconsistent between the foreground write (page-only) and the regen write (full envelope) for the same entry kind.

I confirmed this is not currently live-observable for non-fallback gSP pages: the fresh/stale cache-HIT short-circuits are gated on !isDataReq, so _next/data requests always bypass the HIT and regenerate the full envelope, and the only path that reads pageData back as the data response (generatedFromDataRequest entries) correctly stores renderProps. So no app-level props are dropped today. But the foreground vs regen pageData divergence is fragile — a future change that reads pageData back on a non-regen entry (e.g. an ISR data HIT optimization) would silently drop app-level props. Worth either aligning both foreground writes to store renderProps, or a one-line comment on each foreground write explaining why page-only pageData is intentional there. (Both foreground-write lines are unchanged context, so I couldn't attach inline comments there.)

App Router rewrite-param-to-query change has no App Router test

The PR routes App Router beforeFiles/afterFiles/fallback rewrites through the new applyInternalRewriteDestination, which appends unused rewrite source params to the destination query and mutates url.search (feeding App Router searchParams). mergeRewriteQuery correctly preserves the original query/_rsc, so it's behaviorally sound, but coverage exists only for Pages Router (pages-request-pipeline.test.ts, pages-router.test.ts) and the matcher (shims.test.ts). app-router-next-config-dev.test.ts has no case asserting that a rewrite's unused source params reach searchParams (or that _rsc survives). Please add one to that existing harness so this cross-cutting App Router code path is protected against regression.

);
if (beforeFilesRewrite instanceof Response) return beforeFilesRewrite;
if (beforeFilesRewrite) cleanPathname = beforeFilesRewrite;
if (beforeFilesRewrite) cleanPathname = applyInternalRewriteDestination(beforeFilesRewrite, url);

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.

New App Router behavior with no App Router test: routing beforeFiles/afterFiles/fallback rewrites through applyInternalRewriteDestination now appends unused rewrite source params to the destination query and mutates url.search, which feeds App Router searchParams. The behavior is sound (mergeRewriteQuery preserves the original query/_rsc), but only Pages Router + the matcher are tested. Please add a case to app-router-next-config-dev.test.ts asserting a config rewrite like { source: "/blog-:param", destination: "/blog/post-3" } exposes ?param=... to the App Router page's searchParams and that _rsc survives.

`,
);
fs.writeFileSync(
path.join(rootDir, "pages", "something.jsx"),

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.

The something.jsx fixture is written here but never requested — the GSSP context test (line 1517) only fetches /blog/post-1, /blog-post-2, /blog-post-3, and /rewrite-source/foo. Its getServerSideProps({ query }) / asPath-with-query assertions never run. Either add a /something?... fetch + assertions (it looks intended to cover the query-merge case) or drop the fixture. This has persisted across all prior review passes.

}

const { route, params } = match;
req.url = originalUrl;

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.

req.url = originalUrl mutates the incoming request unconditionally for every matched route, not just GSSP/data requests. For the common case originalUrl defaults to url so it's a no-op, but it is a side effect on shared request state read again later (the regen snapshot regenReq = { url: req.url, ... }). It is intentional — it implements the Next.js req.url contract for data-fetching methods (the data test asserts appProps.url === "/_next/data/.../blog/post-1.json?hello=world"). A one-line comment would prevent a future reader from mistaking it for an accidental reassignment.

Suggested change
req.url = originalUrl;
// Implements the Next.js `req.url` contract: data-fetching methods
// (gSSP/gSP `_app.getInitialProps`) observe the original request URL.
// Defaults to `url` for non-data requests, so this is a no-op there.
req.url = originalUrl;

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #1996.

Summary

I reviewed PR #1996 (fix(pages): preserve app props for GSSP requests) and posted a batch review with a summary plus 3 inline comments. Nothing blocking.

What I independently verified (reading source, not trusting prior passes):

  1. Dev/prod parity for app props threading holds across dev server, prod resolver, prod page handler, and client router shim. The two parity bugs from earlier passes are confirmed fixed in the current head (dev + prod stale-regen both write the full freshRenderProps envelope; the fallback-persist isrSet passes options.expireSeconds).

  2. config-matchers.ts rewrite-param-to-query faithfully mirrors Next.js prepare-destination.ts, including the getDestinationPathAndHost scheme-stripping.

  3. Ran pages-page-data (43), pages-get-initial-props (5), pages-request-pipeline (44) locally — all green.

Findings flagged:

  1. New internal-consistency item: Both foreground ISR writes (dev dev-server.ts:1786, prod pages-page-response.ts:616) store page-only pageData, while the stale-regen paths store the full envelope and the cached HTML embeds the full envelope. I confirmed this is not live-observable today (the fresh/stale HIT short-circuits are gated on !isDataReq, so _next/data always regenerates; only generatedFromDataRequest entries read pageData back and those store renderProps), but it's a fragile foreground/regen divergence worth aligning or commenting. Both lines are unchanged context, so I put this in the review body.

  2. App Router coverage gap (inline): applyInternalRewriteDestination is a new cross-cutting App Router behavior (appending unused rewrite source params to searchParams) with no App Router test — only Pages Router + the matcher are covered. Recommended adding a case to app-router-next-config-dev.test.ts.

  3. Persistent minor items (inline): the unused something.jsx fixture (dead test setup, flagged across all prior passes) and the undocumented unconditional req.url = originalUrl mutation (intentional; suggested a clarifying comment).

Note: two prior inline comment targets (pages-page-response.ts:616, dev-server.ts:1786) were unchanged context lines that GitHub rejected with 422, so I folded those into the review body rather than retrying inline.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx james-elicx merged commit b7ef466 into cloudflare:main Jun 14, 2026
44 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