* feat(ppr): add fallback shell payload identity
* refactor(ppr): address review feedback on payload identity helpers
- normalizePregeneratedPathname: document the deliberate non-strict vs
strict normalization choice so it isn't "fixed" into a build-time throw
- addPregeneratedConcretePath: normalize the pathname internally as the
single source of truth, removing the caller pre-normalize footgun
- export PrerenderRouteParamsRouteMatch for #1716 consumers
- cover direct un-normalized recording with a test
* fix(ppr): mark PrerenderRouteParamsRouteMatch @public for knip
The type is exported for #1716's serving consumers but has no in-repo
reference yet, so knip's no-unused-exports gate flags it. Knip excludes
exports tagged @public from that report; this keeps the reviewer-requested
export without failing CI.
* docs(ppr): document live-Set and dedup contracts on concrete-path registry
Address bonk review notes:
- getRenderedConcreteUrlPathsForRoute returns the live backing Set for
allocation-free hot-path lookups; document that callers must not retain
the reference across a re-seed (clear empties the map, stranding it).
- parsePregeneratedConcretePaths intentionally does not dedupe repeated
route patterns; document that they merge additively and value-dedup in
addPregeneratedConcretePath makes the result equivalent.
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): prerender cacheComponents root-param fallback shells
PR 1 of 4: core model + build integration.
Model: app-ppr-fallback-shell, pregenerated-concrete-paths, prerender-manifest
Build: prerender/run-prerender fallback shell artifact generation
Tests: createAppPprFallbackShells, pregenerated-concrete-paths core
* test(prerender-route-params): add unit tests for fallbackParamNames and validation
Adds focused unit tests for the new prerender-route-params logic introduced
in this PR:
- encodePrerenderRouteParams round-trips fallbackParamNames and omits
them when empty.
- prerenderRouteParamsPayloadMatchesRoute rejects payloads whose
fallbackParamNames contain unknown params or duplicates.
- prerenderRouteParamsPayloadMatchesRoute returns false for valid
fallback-shell matches (only exact matches are accepted today).
Also adds a comment in app-ppr-fallback-shell.ts noting that placeholder
brackets in shell pathnames are percent-encoded by new URL() at fetch
time, so the render path must supply params via the prerender-params
header rather than URL matching.
* docs(prerender): clarify BUILD_ID reuse and fallback-shell param-resolution seams
Address review feedback on PR #1702:
- Explain why run-prerender reuses the built BUILD_ID instead of
re-resolving one (config.buildId feeds prerendered-output identity).
- Note that the fallback-shell prerender-params header path described in
app-ppr-fallback-shell.ts is honored by the #1715 follow-up; this
generation-only PR still resolves params from the URL placeholder.
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): preserve encoded prerender params
App Router prerendered pages with encoded dynamic params could be bypassed in production and rendered again with decoded route params. This diverged from Next.js for generateStaticParams values like "sticks & stones".
The root cause was split across build and startup. The prerender render path only had the encoded URL, so normal route matching decoded the page params. After preserving encoded params for the render, production startup still seeded the ISR cache under the encoded artifact path while live requests lookup cache entries by the runtime-normalized pathname.
Carry trusted prerender params through the prerender-only request path, keep decoded params for dynamicParams validation, and seed prerendered App Router artifacts under the normalized runtime cache key while reading files from their encoded output path.
* chore(app-router): clean up prerender header internals
Reuse the shared record guard for prerender route param parsing and remove the local duplicate.
Keep Next.js INTERNAL_HEADERS as the exact upstream list, with vinext-only internal headers tracked separately but stripped by the same request pipeline. This fixes the CI unit failure while preserving the prerender header trust boundary.
Factor repeated Node-header conversion and forwarded-protocol handling in the production server to avoid local assertions and duplicate loops.
* fix(app-router): scope prerender params to route pattern
Bind the internal prerender route-param header to the route pattern that produced it so encoded render params cannot cross a rewrite boundary into a different dynamic route shape.
The App Router handler now only applies encoded prerender params when the payload routePattern matches the final matched route. Otherwise it falls back to the decoded params from normal route matching.
Tighten the header parser to validate the payload shape with Object.entries/Object.keys and add a regression for a prerendered source route rewritten to a dynamic target route with a different param name.
* fix(app-router): verify prerender params match route
Require trusted prerender route params to prove both the final route pattern and the final decoded matched params before using their encoded render values.
This prevents same-pattern rewrites from carrying stale prerender params into a different concrete route match, while keeping valid prerender renders encoded and static validation decoded.
Add handler regression coverage for same-pattern rewrites and helper coverage for catch-all array comparison.
* docs(app-router): clarify prerender route-param trust boundary
The prerender route-param readers and serializer carried implicit
coupling that read as security verification but actually delegated it.
A reader new to readTrustedPrerenderRouteParams could assume it verifies
the prerender secret, when verification happens once upstream at
prod-server's nodeToWebRequest boundary and every downstream read trusts
that. The double read-then-reattach in createAppRscHandler also depends
on the secret header surviving filterInternalHeaders, with no note of
that coupling.
Document the trust contract: secret is verified at the outer boundary,
downstream reads operate on already-trusted requests gated by
VINEXT_PRERENDER=1, and the secret header must stay out of the internal
strip list for the second read to succeed. Also document the
empty-params to null serialization contract.
No behavior change; comments only.