Skip to content

Client navigation breaks (React #412 / RSC 404) after ISR regenerates a route that was prerendered as a fully static redirect() — build-time prefetchDataRoute forces static RSC serving of a now-postponed entry #98555

Description

@dion-codeit

Link to the code that reproduces this issue

https://github.com/dion-codeit/next-ppr-regen-repro

To Reproduce

The repo was created with npx create-next-app --example reproduction-template and runs next@canary (16.4.0-canary.26 when filed; also reproduced on 16.3.2 and 16.3.4). It is a minimal cacheComponents: true app. app/item/[slug]/page.tsx has generateStaticParams → a, b. Item data comes from data/items.json through a 'use cache' function tagged item-. When the item is missing the page calls redirect('/'); when present it renders a child that awaits connection() (a dynamic hole).

  1. pnpm install
  2. bash repro.sh (default file-system cache) or bash repro.sh custom (in-memory cache handler); or run the steps by hand below. Item a is absent in data/items.json during next build, so:
    • /item/b → ◐ Partial Prerender (response: initial, compute: resuming, no prefetchDataRoute)
    • /item/a → ○ fully static 307 redirect (response: complete, compute: static, prefetchDataRoute: "/item/a.rsc", .rsc emitted)
  3. next start, then "publish" item a (add it to data/items.json) and POST /api/revalidate?tag=item-a (revalidateTag(tag, { expire: 0 })), like a CMS webhook would.
  4. Request /item/a:
  5. Or open http://localhost:3123/ in a browser and click Item A (KEEP_SERVER=1 bash repro.sh keeps the server up).

custom mode uses cache-handler.mjs, a ~30-line in-memory cacheHandler that stores exactly what set receives (including rscData/postponed) and returns it from get — the same shape Redis-backed handlers (e.g. @fortedigital/nextjs-cache-handler) produce. bash repro.sh (no argument) uses the default file-system cache.

Current vs. Expected behavior

Current — after the regeneration, the cached entry for /item/a is a Partially Prerendered page (postponed string present). Document requests resume it correctly. RSC navigation requests do not:

  • next@canary (default file-system cache and custom mode) and 16.3.x with custom mode: 200, x-nextjs-postponed: 1, body is the static shell only (rscData of the postponed entry), no dynamic rows are ever streamed. In the browser the navigation hangs, then shows "This page couldn't load" with React error 412 (Connection closed).
  • 16.3.x with the default file-system cache: 404 with x-nextjs-postponed: 1 (the cache reader skips the .rsc file when the entry has postponed, so rscData is undefined). The router silently falls back to a full page load, so every soft navigation to that route becomes an MPA reload.

The control route /item/b (had data at build time) is fine in all versions: its RSC navigation requests are rendered dynamically.

== before publish: HTML /item/a
HTTP/1.1 307 Temporary Redirect
== after publish: HTML /item/a
HTTP/1.1 200 OK
x-nextjs-postponed: 1
dynamic part in HTML (count): 1
== after publish: RSC navigation /item/a
HTTP/1.1 200 OK
x-nextjs-postponed: 1
dynamic part present: NO          <- shell only, never resumed
== control: RSC navigation /item/b
HTTP/1.1 200 OK
dynamic part present: yes

Expected — once the cache entry for the path carries postponed state, RSC navigation requests should be handled like any other PPR route (dynamic render / resume), regardless of how the path was prerendered at build time. Equivalently, the build-time prefetchDataRoute should not permanently pin a path to "serve prebuilt static RSC".

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.4.0: Fri Mar 15 00:11:08 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T8122
  Available memory (MB): 24576
  Available CPU cores: 8
Binaries:
  Node: 24.12.0
  npm: 11.6.2
  Yarn: N/A
  pnpm: 10.26.0
Relevant Packages:
  next: 16.4.0-canary.26 // Latest available version is detected (16.4.0-canary.26).
  eslint-config-next: N/A
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

cacheComponents, Partial Prerendering (PPR), Linking and Navigating, Dynamic Routes, Redirects

Which stage(s) are affected? (Select all that apply)

next build (local), next start (local), Other (Deployed)

Additional context

Where this happens (next/dist/build/templates/app-page-runtime.js, source packages/next/src/build/templates/app-page.ts; same code on canary):

const staticPrefetchDataRoute = prerenderManifest.routes[resolvedPathname]?.prefetchDataRoute
let isDynamicRSCRequest = isRoutePPREnabled && isRSCRequest && !isPrefetchRSCRequest &&
  // If generated at build time, treat the RSC request as static
  // so we can serve the prebuilt .rsc without a dynamic render.
  !staticPrefetchDataRoute

prefetchDataRoute only exists for paths that were fully static at build time and is never re-evaluated. Later, with the regenerated entry:

if (didPostpone && !isDynamicRSCRequest) res.setHeader('x-nextjs-postponed', '1')
if (isRSCRequest && !isDraftMode) {
  if (typeof cachedData.rscData === 'undefined') { /* cacheComponents → 404 */ }
  // "As this isn't a prefetch request, we should serve the static flight data."
  return sendRenderResult({ result: RenderResult.fromStatic(cachedData.rscData, RSC_CONTENT_TYPE_HEADER) })
}
// ... the HTML branch below resumes `cachedData.postponed`; the RSC branch never does.

The 404 variant on 16.3.x comes from server/lib/incremental-cache/file-system-cache.js, which only reads <key>.rsc when !isRoutePPREnabled || meta.postponed == null.

Real-world trigger: a track/product page with generateStaticParams whose data is not published yet at deploy time (redirect() or notFound() at the page top level), published later and revalidated by tag. We hit this in production-like environments with a Redis ISR cache handler; a redeploy "fixes" it because the manifest then records the path as partially static. Current workaround: exclude such params from generateStaticParams so the path never gets a prefetchDataRoute.

Possibly related but different: opennextjs/opennextjs-cloudflare#1223 (adapter-side rsc undefined for PPR entries).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Dynamic RoutesRelated to dynamic routes.Linking and NavigatingRelated to Next.js linking (e.g., <Link>) and navigation.RedirectsRelated to redirecting.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions