cloudflare / cloudflare/vinext
App Router cacheComponents: revisit notFound recovery — upstream reverts boundary-rerender, treats error page as resumable
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Next.js Change
**Commit:** [`0f35dab`](https://github.com/vercel/next.js/commit/0f35dab6e6839af3e4a1fea12a6dad55f35d97fd)
**PR:** [#94037 — Improve notFound recovery in error pathway](https://github.com/vercel/next.js/pull/94037)
**Reverts** [#92231](https://github.com/vercel/next.js/pull/92231) and **relands** [#93988](https://github.com/vercel/next.js/pull/93988).
## Background
#764 in this repo tracked the original Next.js implementation in PR #92231, which introduced a `findPrerenderHTTPErrorBoundaryTree` walk and a `PrerenderHTTPErrorState` rerender to handle `notFound()` / `forbidden()` / `unauthorized()` thrown during the cacheComponents prerender. That approach has now been **reverted upstream** and replaced with a different strategy.
## What changed in this commit
The diff backs out the `findPrerenderHTTPErrorBoundaryTree` / `PrerenderHTTPErrorState` machinery entirely:
```diff
-import {
- createComponentTree,
- getRootParams,
- type PrerenderHTTPErrorState,
-} from './create-component-tree'
+import { createComponentTree, getRootParams } from './create-component-tree'
```
```diff
-type HTTPAccessErrorStatusCode = 404 | 403 | 401
-
-function hasPrerenderHTTPErrorBoundary(...) { ... }
-function findPrerenderHTTPErrorBoundaryTree(...) { ... }
```
```diff
async function getRSCPayload(
...
options: {
is404: boolean
- // When set, rerender a segment-scoped HTTP fallback inside the normal app
- // router tree instead of falling back to the generic error shell payload.
- prerenderHTTPError?: PrerenderHTTPErrorState
...
}
)
```
PR description (verbatim):
> Prerendering the notFound loaderTree turns out to not be the correct behavior in many cases. The original motivation of 92231 was to solve the connection closed problem caused by statically prerendering an incomplete HTTP event like notFound() when cacheComponents is enabled because it would leave holes in the inlined flight data that never resolved and led to reported errors on hydration.
>
> This change now continues to fork the CC behavior of the error handler but it treats the error page as a resumable page so fresh hydration data is generated if it wasn't able to be entirely static.
>
> This is good for correctness but it also means the page is not going to statically prerender the notFound UI. This will be handled in a followup.
A new helper, `continueStaticPrerenderWithInlinedData`, is introduced (+~80 LoC) to drive this "treat-as-resumable" path. When `fallbackRouteParams` are present, an empty `InitialRSCPayload` is sent to the server-component renderer and the actual data is deferred to the client, which parses the params from the URL and hydrates:
```ts
// New helper added in app-render.tsx (excerpt)
async function continueStaticPrerenderWithInlinedData(...) {
const hasFallbackRouteParams =
fallbackRouteParams && fallbackRouteParams.size > 0
if (hasFallbackRouteParams) {
// Rather than use a dynamic server resume to fill in the params,
// we can rely on the client to parse the params from the URL and use
// that to hydrate the page.
//
// Send an empty InitialRSCPayload to the server component renderer
// The data will be fetched by the client instead.
const emptyReactServerResult = ...
}
}
```
The new test fixtures under `test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/` cover four shapes:
- `not-found-above-boundary/` — `notFound()` thrown above a `not-found.tsx` boundary
- `not-found-dynamic-flight/[slug]/` — dynamic flight with parallel route default
- `not-found-static-flight/[slug]/` — static flight with parallel route default
- `not-found-use-params/[slug]/` — `not-found.tsx` that calls `useParams()`
## Trade-off the PR explicitly calls out
The new approach is **good for correctness** (no more `Connection closed` hydration failures) but **gives up static prerendering of the notFound UI**. A followup will reintroduce static prerendering of the notFound UI on top of this corrected base.
## Impact on vinext
vinext has its own App Router prerender pipeline. The relevant code in `packages/vinext/src/server/app-page-execution.ts` already models an `http-access-fallback` outcome:
```ts
| { kind: "http-access-fallback"; statusCode: number; fromMetadata?: boolean };
```
Issue #764 is currently closed, but the implementation strategy it described (find the deepest matching fallback boundary, rerender the normal app router RSC payload scoped to that boundary) is now **not** the upstream direction. If vinext implemented #764 along the lines of PR #92231, that implementation is now upstream-divergent and should be revisited. If vinext has not yet implemented #764, it should adopt the new "resumable error page" model directly rather than the reverted boundary-rerender model.
Concrete things vinext likely needs to mirror:
1. **Stop prerendering the notFound loaderTree** in the cacheComponents prerender recovery path. The notFound page should be treated as a resumable page, not as a static rerender of a boundary subtree.
2. **Generate fresh hydration data** in the error recovery path when the original prerender was not fully static — do not reuse the aborted prerender prelude's flight data.
3. **Defer to the client** when `fallbackRouteParams` are non-empty: emit an empty `InitialRSCPayload` and let the client parse params from the URL and hydrate. (vinext's existing thenable params shim, `Object.assign(Promise.resolve(params), params)`, should compose with this.)
4. **Followup parity (deferred upstream)**: once Next.js lands the followup that re-adds static prerendering of the notFound UI on top of this corrected base, mirror that as well.
## Status of #764
#764 should be re-evaluated against the new upstream direction. The status-quo answer to "how should `notFound()` during cacheComponents prerender be handled?" has changed.
## Related
- #764 — closed; tracked the now-reverted approach from PR #92231
- #989 — falsy thrown values in error boundaries
- #346 — `renderHTTPAccessFallbackPage` params passing
Contributor guide
Assessment
This issue has not been assessed yet.