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).
pnpm install
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)
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.
- Request /item/a:
- 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).
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).
pnpm installbash repro.sh(default file-system cache) orbash 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: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.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/ais a Partially Prerendered page (postponedstring present). Document requests resume it correctly. RSC navigation requests do not:next@canary(default file-system cache andcustommode) and 16.3.x withcustommode:200,x-nextjs-postponed: 1, body is the static shell only (rscDataof 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).404withx-nextjs-postponed: 1(the cache reader skips the.rscfile when the entry haspostponed, sorscDatais 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.Expected — once the cache entry for the path carries
postponedstate, 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-timeprefetchDataRouteshould 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/AWhich 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, sourcepackages/next/src/build/templates/app-page.ts; same code on canary):prefetchDataRouteonly exists for paths that were fully static at build time and is never re-evaluated. Later, with the regenerated entry:The 404 variant on 16.3.x comes from
server/lib/incremental-cache/file-system-cache.js, which only reads<key>.rscwhen!isRoutePPREnabled || meta.postponed == null.Real-world trigger: a track/product page with
generateStaticParamswhose data is not published yet at deploy time (redirect()ornotFound()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 fromgenerateStaticParamsso the path never gets aprefetchDataRoute.Possibly related but different: opennextjs/opennextjs-cloudflare#1223 (adapter-side
rscundefined for PPR entries).