Prevent synchronous IO from entering Cached Navigations - #98511
Conversation
Tests PassedCommit: df000df |
## Summary Separate build-time prerender selection from runtime parameter matching behind `experimental.paramMatching`. `generateStaticParams` still chooses concrete build-time examples; `experimental_paramMatching` and the argument-free `experimental_generateParamMatching()` configure visible parameters as `not-found`, `blocking`, `fallback`, or permanently `dynamic`. ## Temporary stack foundation Build on #98512 (and its prerequisite #98511), replacing our separate dev validation fix in #98460. Staged rendering and static-shell validation share Hendrik's selected-shell parameter set. This layer adds explicit fallback boundaries without cloning the render context or introducing a second validation-only parameter set. ## Example For `/[lang]/catalog/[top]/items/[bottom]`: ```ts // app/[lang]/layout.tsx export const experimental_paramMatching = { lang: 'not-found', } as const // app/[lang]/catalog/[top]/items/[bottom]/page.tsx export const experimental_paramMatching = { top: 'blocking', bottom: 'fallback', } as const export function generateStaticParams() { return [{ lang: 'en', top: 't1', bottom: 'b1' }] } ``` A novel `lang` returns 404, a novel `top` blocks on generation, and a novel `bottom` can receive fallback UI immediately. `/en/catalog/t1/items/b1` is prerendered during the build. Closed prefixes use the existing fallback-false adapter contract so the prototype does not require a Vercel proxy change. ## Composition and validation - Layouts and pages may configure only parameters visible at or above their own segment; `[lang]/layout.tsx` cannot configure the later `top` parameter. - Merge ancestor-to-descendant fragments by assignment. Generated fragments run independently and receive no parent argument. - Validate explicit directives in `not-found -> blocking -> fallback -> dynamic` order before filling unconfigured holes using the shell heuristic. Do not silently rewrite inherited directives to repair incoherent overrides. - Validate that parallel branches contributing to the same URL matcher agree. - Reject build-time examples for explicitly dynamic parameters and constrain static export keys through generated types. ## Static shells and development Explicit fallback shells must contain useful static UI rather than silently becoming blocking. Blocking policies also require a useful reachable shell, unless the route opts out with `instant = false`. Without an example reaching the configured boundary, validate the reachable generic shell. Blocking-only validation renders are not retained as cache outputs, and redundant generic blocking renders are omitted when a more-specific validation covers them. Development retains #98512's required-or-completed shell selection, including mixed-depth branches. An explicit fallback adds its parameter and the suffix to that staged set, even for a fully generated URL. For example, with a `t1/b1` prerender and `top: 'fallback'`, requests for `t1/b1`, `t1/b2`, and `t2/b2` all validate with both parameters unknown. Existing holes in a required partial shell are preserved. Ordinary dev requests remain dynamic renders. Matcher-only entries have no source artifact to validate; their runtime metadata explicitly stages the completed shell, retaining any dynamic tail. ## Coverage Fixtures cover inherited and overridden policies, inferred holes, runtime-only generation, catch-all routes, parallel slots, foreground cache behavior, development validation, export constraints, type generation, and invalid configurations. The separate diagnostic commit remains optional and removable.
0412531 to
14255e5
Compare
## Summary Separate build-time prerender selection from runtime parameter matching behind `experimental.paramMatching`. `generateStaticParams` still chooses concrete build-time examples; `experimental_paramMatching` and the argument-free `experimental_generateParamMatching()` configure visible parameters as `not-found`, `blocking`, `fallback`, or permanently `dynamic`. ## Temporary stack foundation Build on #98512 (and its prerequisite #98511), replacing our separate dev validation fix in #98460. Staged rendering and static-shell validation share Hendrik's selected-shell parameter set. This layer adds explicit fallback boundaries without cloning the render context or introducing a second validation-only parameter set. ## Example For `/[lang]/catalog/[top]/items/[bottom]`: ```ts // app/[lang]/layout.tsx export const experimental_paramMatching = { lang: 'not-found', } as const // app/[lang]/catalog/[top]/items/[bottom]/page.tsx export const experimental_paramMatching = { top: 'blocking', bottom: 'fallback', } as const export function generateStaticParams() { return [{ lang: 'en', top: 't1', bottom: 'b1' }] } ``` A novel `lang` returns 404, a novel `top` blocks on generation, and a novel `bottom` can receive fallback UI immediately. `/en/catalog/t1/items/b1` is prerendered during the build. Closed prefixes use the existing fallback-false adapter contract so the prototype does not require a Vercel proxy change. ## Composition and validation - Layouts and pages may configure only parameters visible at or above their own segment; `[lang]/layout.tsx` cannot configure the later `top` parameter. - Merge ancestor-to-descendant fragments by assignment. Generated fragments run independently and receive no parent argument. - Validate explicit directives in `not-found -> blocking -> fallback -> dynamic` order before filling unconfigured holes using the shell heuristic. Do not silently rewrite inherited directives to repair incoherent overrides. - Validate that parallel branches contributing to the same URL matcher agree. - Reject build-time examples for explicitly dynamic parameters and constrain static export keys through generated types. ## Static shells and development Explicit fallback shells must contain useful static UI rather than silently becoming blocking. Blocking policies also require a useful reachable shell, unless the route opts out with `instant = false`. Without an example reaching the configured boundary, validate the reachable generic shell. Blocking-only validation renders are not retained as cache outputs, and redundant generic blocking renders are omitted when a more-specific validation covers them. Development retains #98512's required-or-completed shell selection, including mixed-depth branches. An explicit fallback adds its parameter and the suffix to that staged set, even for a fully generated URL. For example, with a `t1/b1` prerender and `top: 'fallback'`, requests for `t1/b1`, `t1/b2`, and `t2/b2` all validate with both parameters unknown. Existing holes in a required partial shell are preserved. Ordinary dev requests remain dynamic renders. Matcher-only entries have no source artifact to validate; their runtime metadata explicitly stages the completed shell, retaining any dynamic tail. ## Coverage Fixtures cover inherited and overridden policies, inferred holes, runtime-only generation, catch-all routes, parallel slots, foreground cache behavior, development validation, export constraints, type generation, and invalid configurations. The separate diagnostic commit remains optional and removable.
| stageController.getSyncInterruptReason() | ||
| ? 0 | ||
| : byteLengths[RenderStage.Static] |
There was a problem hiding this comment.
why is this needed? StagedRenderingController already gets out of Static when we hit sync IO:
next.js/packages/next/src/server/app-render/staged-rendering.ts
Lines 176 to 180 in afb0dba
so any content emitted after twouldn't make it into the cached nav anyway
and just because it was aborted prematurely doesn't mean it'll be useless, because it may have been in a suspense boundary. sure, we'd error for this in build, but having some content seems better than none at all?
There was a problem hiding this comment.
oh did this hang in decodeBufferedStage because the root is blocked? in that case we should probably just reject the decode after setTimeout(_, 0), that's not the only scenario that can block the root. e.g. we can have
export default async function RootLayout({ children }) {
// not blocking during build, blocking after revalidation
if (await cacheThatBecomesTrueAfterRevalidation()) {
await dynamicData()
}
// ...
}which (off the top of my head, untested) should be fixed by something like this:
export function decodeBufferedStage<T>(
buffer: Uint8Array,
headers: RequestHeaders | undefined
): Promise<T> {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(buffer)
controller.close()
},
})
return new Promise<T>((resolve, reject) => {
// The stream is buffered, so the decode should yield a root microtaskily.
// If it doesn't, then the root must be blocked.
const timeout = setTimeout(() => {
reject(new Error('Root is blocked.'))
stream.cancel()
});
createFromNextReadableStream<T>(stream, headers, {
allowPartialStream: true,
}).then((root) => {
clearTimeout(timeout)
resolve(root);
}, reject);
});
}(we may also just want to return a hanging promise, but if we know it's useless bc it blocks, then i guess there's no point)
| syncIO: SyncIOMode.Untracked, | ||
| // Synchronous request-time data ends the static stage before its result can | ||
| // enter the Cached Navigation. | ||
| syncIO: SyncIOMode.AllowedInRuntimeOrDynamic, |
There was a problem hiding this comment.
may want to use getSyncIOMode(prefetchMode) for consistency. in PPF we only allow sync IO in the dynamic stage so if we hit some at any point we should go straight to dynamic
...not that it matters very much, because we don't care about recovering stages past Static here anyway, but at least then it won't look like an exception when it really doesn't need to be
Cached Navigations incorrectly reused uncached timestamps as static page content. A repeat visit showed the previous request's `Date.now()` result while the new response was still pending. This affected both dynamic RSC responses and the RSC payload embedded in the initial HTML. The production renderer left synchronous IO untracked because it assumed the build had already checked it. A private cache exposed the gap: the static prerender suspended at the cache call and never reached the `Date.now()` after it. A production request resolved the private cache during the static stage and included the uncached timestamp in the reusable response. Production staged renders now end the static stage when synchronous request-time IO occurs. They mark the interrupted static and shell prefixes as empty, while the rest of the response renders normally. The client skips those prefixes instead of waiting for a Flight root that the interrupted stage may not have emitted. The regression tests cover dynamic RSC navigation, initial HTML hydration, and full prefetching with a paramless private-cache fixture.
Sync interruptions currently discard the entire static prefix, including usable content. This change preserves that content and bounds optional prefix decoding so an unresolved Flight root cannot block navigation.
## Summary Separate build-time prerender selection from runtime parameter matching behind `experimental.paramMatching`. `generateStaticParams` still chooses concrete build-time examples; `experimental_paramMatching` and the argument-free `experimental_generateParamMatching()` configure visible parameters as `not-found`, `blocking`, `fallback`, or permanently `dynamic`. ## Temporary stack foundation Build on #98512 (and its prerequisite #98511), replacing our separate dev validation fix in #98460. Staged rendering and static-shell validation share Hendrik's selected-shell parameter set. This layer adds explicit fallback boundaries without cloning the render context or introducing a second validation-only parameter set. ## Example For `/[lang]/catalog/[top]/items/[bottom]`: ```ts // app/[lang]/layout.tsx export const experimental_paramMatching = { lang: 'not-found', } as const // app/[lang]/catalog/[top]/items/[bottom]/page.tsx export const experimental_paramMatching = { top: 'blocking', bottom: 'fallback', } as const export function generateStaticParams() { return [{ lang: 'en', top: 't1', bottom: 'b1' }] } ``` A novel `lang` returns 404, a novel `top` blocks on generation, and a novel `bottom` can receive fallback UI immediately. `/en/catalog/t1/items/b1` is prerendered during the build. Closed prefixes use the existing fallback-false adapter contract so the prototype does not require a Vercel proxy change. ## Composition and validation - Layouts and pages may configure only parameters visible at or above their own segment; `[lang]/layout.tsx` cannot configure the later `top` parameter. - Merge ancestor-to-descendant fragments by assignment. Generated fragments run independently and receive no parent argument. - Validate explicit directives in `not-found -> blocking -> fallback -> dynamic` order before filling unconfigured holes using the shell heuristic. Do not silently rewrite inherited directives to repair incoherent overrides. - Validate that parallel branches contributing to the same URL matcher agree. - Reject build-time examples for explicitly dynamic parameters and constrain static export keys through generated types. ## Static shells and development Explicit fallback shells must contain useful static UI rather than silently becoming blocking. Blocking policies also require a useful reachable shell, unless the route opts out with `instant = false`. Without an example reaching the configured boundary, validate the reachable generic shell. Blocking-only validation renders are not retained as cache outputs, and redundant generic blocking renders are omitted when a more-specific validation covers them. Development retains #98512's required-or-completed shell selection, including mixed-depth branches. An explicit fallback adds its parameter and the suffix to that staged set, even for a fully generated URL. For example, with a `t1/b1` prerender and `top: 'fallback'`, requests for `t1/b1`, `t1/b2`, and `t2/b2` all validate with both parameters unknown. Existing holes in a required partial shell are preserved. Ordinary dev requests remain dynamic renders. Matcher-only entries have no source artifact to validate; their runtime metadata explicitly stages the completed shell, retaining any dynamic tail. ## Coverage Fixtures cover inherited and overridden policies, inferred holes, runtime-only generation, catch-all routes, parallel slots, foreground cache behavior, development validation, export constraints, type generation, and invalid configurations. The separate diagnostic commit remains optional and removable.
14255e5 to
df000df
Compare
## Summary Separate build-time prerender selection from runtime parameter matching behind `experimental.paramMatching`. `generateStaticParams` still chooses concrete build-time examples; `experimental_paramMatching` and the argument-free `experimental_generateParamMatching()` configure visible parameters as `not-found`, `blocking`, `fallback`, or permanently `dynamic`. ## Temporary stack foundation Build on #98512 (and its prerequisite #98511), replacing our separate dev validation fix in #98460. Staged rendering and static-shell validation share Hendrik's selected-shell parameter set. This layer adds explicit fallback boundaries without cloning the render context or introducing a second validation-only parameter set. ## Example For `/[lang]/catalog/[top]/items/[bottom]`: ```ts // app/[lang]/layout.tsx export const experimental_paramMatching = { lang: 'not-found', } as const // app/[lang]/catalog/[top]/items/[bottom]/page.tsx export const experimental_paramMatching = { top: 'blocking', bottom: 'fallback', } as const export function generateStaticParams() { return [{ lang: 'en', top: 't1', bottom: 'b1' }] } ``` A novel `lang` returns 404, a novel `top` blocks on generation, and a novel `bottom` can receive fallback UI immediately. `/en/catalog/t1/items/b1` is prerendered during the build. Closed prefixes use the existing fallback-false adapter contract so the prototype does not require a Vercel proxy change. ## Composition and validation - Layouts and pages may configure only parameters visible at or above their own segment; `[lang]/layout.tsx` cannot configure the later `top` parameter. - Merge ancestor-to-descendant fragments by assignment. Generated fragments run independently and receive no parent argument. - Validate explicit directives in `not-found -> blocking -> fallback -> dynamic` order before filling unconfigured holes using the shell heuristic. Do not silently rewrite inherited directives to repair incoherent overrides. - Validate that parallel branches contributing to the same URL matcher agree. - Reject build-time examples for explicitly dynamic parameters and constrain static export keys through generated types. ## Static shells and development Explicit fallback shells must contain useful static UI rather than silently becoming blocking. Blocking policies also require a useful reachable shell, unless the route opts out with `instant = false`. Without an example reaching the configured boundary, validate the reachable generic shell. Blocking-only validation renders are not retained as cache outputs, and redundant generic blocking renders are omitted when a more-specific validation covers them. Development retains #98512's required-or-completed shell selection, including mixed-depth branches. An explicit fallback adds its parameter and the suffix to that staged set, even for a fully generated URL. For example, with a `t1/b1` prerender and `top: 'fallback'`, requests for `t1/b1`, `t1/b2`, and `t2/b2` all validate with both parameters unknown. Existing holes in a required partial shell are preserved. Ordinary dev requests remain dynamic renders. Matcher-only entries have no source artifact to validate; their runtime metadata explicitly stages the completed shell, retaining any dynamic tail. ## Coverage Fixtures cover inherited and overridden policies, inferred holes, runtime-only generation, catch-all routes, parallel slots, foreground cache behavior, development validation, export constraints, type generation, and invalid configurations. The separate diagnostic commit remains optional and removable.
Cached Navigations incorrectly reused uncached timestamps as static page content. A repeat visit showed the previous request's
Date.now()result while the new response was still pending. This affected both dynamic RSC responses and the RSC payload embedded in the initial HTML.The production renderer left synchronous IO untracked because it assumed the build had already checked it. A private cache exposed the gap: the static prerender suspended at the cache call and never reached the
Date.now()after it. A production request resolved the private cache during the static stage and included the uncached timestamp in the reusable response.Production staged renders now end the static stage when synchronous request-time IO occurs. They mark the interrupted static and shell prefixes as empty, while the rest of the response renders normally. The client skips those prefixes instead of waiting for a Flight root that the interrupted stage may not have emitted.
The regression tests cover dynamic RSC navigation, initial HTML hydration, and full prefetching with a paramless private-cache fixture.