## Summary
- Backport of #92123 to `next-16-2` release branch
- Cherry-picked the CSS HMR Safari fix and regenerated turbopack-tests
snapshot goldens for the release branch
## Test plan
- [x] `UPDATE=1 cargo nextest r -p turbopack-tests` — all 297 tests pass
- [x] `pnpm build-all` — clean build, no issues with
`generated-native.d.ts`
### What?
Metadata routes (`manifest.ts`, `robots.ts`, `sitemap.ts`, `icon.tsx`,
`apple-icon.tsx`, etc.) were not being hot-reloaded in Turbopack dev
mode — changes to those files would not be reflected on subsequent
requests until a full server restart.
### Why?
PR #91466 extended `usesServerHmr = true` in `clearRequireCache()` (in
`hot-reloader-turbopack.ts`) from `app-page` entries only to **all**
`app`-type entries (pages + route handlers). The motivation was correct:
regular route handlers like `app/api/hello/route.ts` use Turbopack's
in-place module update model and benefit from server HMR.
However, metadata routes (`/manifest.webmanifest/route`,
`/robots.txt/route`, etc.) are also `app`-type entries but they are
**not** suitable for in-place server HMR. When `usesServerHmr = true`
for a metadata route, `clearRequireCache()` skips two critical
invalidation steps:
1. Deleting the compiled chunk from `require.cache`
2. Calling `__next__clear_chunk_cache__()`
Without those steps, the old module stays in-memory and all subsequent
requests to `/manifest.webmanifest` (etc.) return the stale content.
### How?
Added an `!isMetadataRoute(entryPage)` guard to the `usesServerHmr`
expression in `clearRequireCache()`. This restores full cache
invalidation for metadata routes on every rebuild while leaving regular
route handler server HMR (added in #91466) intact.
```ts
// Before
const usesServerHmr =
serverFastRefresh &&
entryType === 'app' &&
writtenEndpoint.type !== 'edge'
// After
const usesServerHmr =
serverFastRefresh &&
entryType === 'app' &&
writtenEndpoint.type !== 'edge' &&
!isMetadataRoute(entryPage) // ← metadata routes always clear the cache
```
`isMetadataRoute('/manifest.webmanifest/route')` → `true` (excluded from
server HMR)
`isMetadataRoute('/api/hello/route')` → `false` (keeps server HMR, no
regression)
Also added a regression test: `metadata route hmr > reflects manifest.ts
changes on fetch/refresh` in the `server-hmr` test suite, with a
`manifest.ts` fixture that starts at `name: 'Version 0'`. The test
patches the file and asserts the updated JSON is returned on the next
fetch.
Fixes#91981
---------
Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
App router handlers are already built with the Turbopack runtime, so
they can use server HMR. **We also already get subscription events for
their chunks.** However, unlike app pages which use
`__next_app__.require()` for dynamic devModuleCache lookup, route
handlers capture userland exports statically in AppRouteRouteModule at
construction time. This change makes routes behave a lot like pages,
dynamically loading the user's code when requests are made. This way, we
can freely invalidate and reload it when changes are made without
evicting the entire require cache.
Only `next dev` uses this lazy evaluation, as `next start` continues to
eagerly import route handlers.
This change removes the `isAppPage` restriction from `usesServerHmr`,
extending server HMR coverage to all App Router entries (pages and route
handlers) built with the Node.js runtime.
For route handlers, we also clear the entry chunk from Node.js
`require.cache` on each rebuild so the next `requirePage()` call
re-executes the entry and obtains fresh module exports from
`devModuleCache` (which HMR updates in-place).
Test Plan: Added an additional e2e test, `with-dep`
### What?
Adds `experimental.serverFastRefresh` to `next.config.js` so users (and
Next.js plugins) can opt out of server-side Fast Refresh without needing
to pass a CLI flag.
Also adds a `--server-fast-refresh` positive CLI flag (hidden) alongside
the existing `--no-server-fast-refresh`, and emits a warning when the
CLI flag and config value conflict. This is a necessary implementation
detail for `commander` to detect when no option is provided.
### Why?
Certain Next.js plugins have encountered issues with server Fast Refresh
and need a way to disable it programmatically via config rather than
requiring users to modify their start scripts. The CLI-only opt-out
(`--no-server-fast-refresh`) was insufficient for this use case.
### How?
- **`config-shared.ts` / `config-schema.ts`**: Added
`experimental.serverFastRefresh?: boolean` to the `ExperimentalConfig`
type and Zod schema.
- **`router-server.ts`**: Resolves the effective `serverFastRefresh`
value at startup by combining the CLI flag (`opts.serverFastRefresh`)
and `nextConfig.experimental.serverFastRefresh`, with CLI taking
precedence. Emits a `Log.warn` when both are set to conflicting values.
Defaults to `true` when neither is specified.
- **`bin/next.ts`**: Fixed the Commander option definition for
`--no-server-fast-refresh`. Commander's `--no-X` pattern implicitly
creates a positive `--server-fast-refresh` option defaulting to `true`,
which meant `opts.serverFastRefresh` was never `undefined` and the
config value could never take effect. Fixed by explicitly registering a
hidden `--server-fast-refresh` option with `.default(undefined)`.
- **Tests**: Added two new test suites in
`test/development/app-dir/server-hmr/server-hmr.test.ts`:
- `server-hmr config opt-out` — verifies that setting
`experimental.serverFastRefresh: false` in config causes unmodified
dependencies to be re-evaluated (i.e., server HMR is disabled).
- `server-hmr CLI/config conflict warning` — verifies that passing
`--no-server-fast-refresh` when config has `serverFastRefresh: true`
logs the conflict warning.
### Test Plan
- [x] Added e2e development tests
(`test/development/app-dir/server-hmr/server-hmr.test.ts`) — all 8 tests
pass
---------
Co-authored-by: Will Binns-Smith <wbinnssmith@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
When AI agents run `create-next-app` with explicit flags like
`--typescript --tailwind --eslint --app --src-dir`, the CLI still enters
interactive mode and prompts for any unspecified options:
```
➜ npx create-next-app my-app --typescript --tailwind --eslint --app --src-dir --use-pnpm
✔ Would you like to use React Compiler? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes
```
Agents can sometimes answer these interactive prompts correctly, but
often they can't. When they fail to navigate the prompts, they fall back
to scaffolding the project themselves from scratch — generating files
based on stale training data. This means they might initialize an app
using deprecated patterns or pin to an older version of Next.js (e.g.
15) instead of the latest. Using `create-next-app` non-interactively is
the best way to ensure agents always produce up-to-date scaffolding.
Previously, `hasProvidedOptions` only skipped the initial "use
recommended defaults?" meta-prompt but still showed individual prompts
for each missing option. Now when any config flags are provided, all
remaining options use the recommended defaults without prompting.
The resolved defaults are printed to stdout so the caller knows exactly
what was assumed and which flags to pass to override:
```
Using defaults for unprovided options:
--eslint ESLint (use --biome for Biome, --no-eslint for None)
--no-react-compiler No React Compiler (use --react-compiler for React Compiler)
--agents-md AGENTS.md (use --no-agents-md for No AGENTS.md)
--import-alias "@/*"
To customize, re-run with explicit flags.
```
The `displayConfig` array is extended with a `flags` field so this
output is auto-generated from the same source of truth used for the
interactive prompts. Existing behavior for `--yes`, CI mode, and fully
interactive mode (no flags) is unchanged.
## Summary
- move adapter implementation docs from
`app/api-reference/config/next-config-js/adapters` to
`app/api-reference/adapters`
- keep `adapterPath` as an entry page that points to the new adapters
section
- add matching Pages Router pointer docs under
`pages/api-reference/adapters`
- update existing docs links that referenced old `adapterPath#...`
anchors to the new adapters routes
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:
## For Contributors
### Improving Documentation
- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide
### Fixing a bug
- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
### Adding a feature
- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
## For Maintainers
- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Closes NEXT-
Fixes #
-->
## What?
Fixes a broken link in the Preserving UI State guide:
https://nextjs.org/docs/app/guides/preserving-ui-state#examples
The "Activity Patterns demo" link currently points to a non-working URL.
## Why?
The demo link returns a 404 / invalid page, which makes the example
section confusing for readers.
## Changes
- Updated the Activity Patterns demo link to the correct URL.
## Notes
No functional changes, docs only.
@icyJoseph @delbaoliveira
## Summary
This fixes adapter `onBuildComplete` outputs for dynamic metadata
routes.
## Bug
`handleBuildComplete` skipped metadata routes too aggressively via
`isStaticMetadataFile(...)`, so dynamic metadata routes (for example
`robots.txt` / `sitemap.xml` using `connection()`) were omitted from
`outputs.appRoutes`.
## Fix
Only skip metadata routes when they are actually prerendered/static
(present in prerender manifests). Dynamic metadata routes are now
included in adapter `outputs.appRoutes` as expected.
## Tests
Added e2e coverage in `test/e2e/app-dir/adapter-dynamic-metadata`:
- verifies dynamic `robots.txt`, `sitemap.xml`, and `favicon.ico`
functionality
- verifies adapter output classification for non-deploy runs
- skips output-shape verification in deploy mode
- skips the suite in dev mode
Otherwise you get a `Dependency tracking is disabled` / infinite loop
when trying to use both turbopack.rules.loader and turbopackLoader at
the same time
The `staticPathKey` condition added in #91231 inadvertently applies to
server action requests on dynamic SSG routes when `cacheComponents` is
enabled.
Server action fetch requests from the client do not send the `RSC`
header (`rsc: 1`). They only send `Accept: text/x-component` and the
`Next-Action` header. This means `isRSCRequest` and
`isDynamicRSCRequest` are both `false` for action requests. The new
`staticPathKey` condition (`isSSG && pageIsDynamic &&
prerenderInfo?.fallbackRouteParams`) evaluates to `true` for dynamic PPR
routes, setting `staticPathKey` even though `ssgCacheKey` is `null` for
actions.
With `staticPathKey` set, the request enters the fallback rendering
block, which serves the cached fallback HTML shell with the action
result appended to it, instead of responding with just the RSC action
result.
The fix excludes server action requests from the `staticPathKey`
computation by adding `!isPossibleServerAction` to the condition,
restoring the pre-#91231 behavior where `staticPathKey` was always
`null` for server actions in production.
fixes#91662closes#91677closes#91669
Remove the `MADV_UNMERGEABLE` (Unmergeable) mmap advice from
`turbo-persistence`'s `advise_mmap_for_persistence` helper in
`mmap_helper.rs`.
The WSL (Windows Subsystem for Linux) kernel does not support
`MADV_UNMERGEABLE` and returns an error when it is applied, causing
turbo-persistence to fail on WSL. See:
https://x.com/von_cronen/status/2034944304712392891
Additionally, `MADV_UNMERGEABLE` is already the **default** state for
memory pages — KSM (Kernel Same-page Merging) is opt-in via
`MADV_MERGEABLE`. Explicitly setting `MADV_UNMERGEABLE` on pages that
were never marked mergeable is a no-op on standard kernels, so the call
provides no benefit while breaking WSL users.
The `MADV_DONTFORK` advice is retained, as it has a clear correctness
benefit (prevents mmap regions from being inherited by child processes
on `fork()`).
Removed the `mmap.advise(memmap2::Advice::Unmergeable)` call and its
associated doc comment from
`turbopack/crates/turbo-persistence/src/mmap_helper.rs`.
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Add `'turbopack-transition': 'next-server-utility'` to all server-side imports in the `app-page.ts` template so they are properly placed in the server utility layer, rather than relying on `SharedMerged` chunk groups to handle them
- Remove the now-unnecessary `next-server-utility` transition from `fillMetadataSegment` import in `app_page_loader_tree.rs`
- Add `SharedMultiple` chunk group variant to support multiple shared entries without a merge tag
- This avoid passing the `parent` into `SharedMerged`, which did break the layout segment optimization.
- Refactor `app_module_graphs` to combine `client_shared_entries` + `has_layout_segments` into a single `Option` parameter, simplifying the API and removing the dead code path for non-page endpoints
## Test plan
- [ ] Verify dev and production builds work with app pages that have layout segments
- [ ] Verify route handlers still build correctly (no client runtime entries passed)
- [ ] Run existing app-dir e2e tests
### What?
Fixed Turbopack incorrectly trying to resolve inline JavaScript code as module references when `new Worker()` is called with `{ eval: true }` option.
Now we skip creating a reference when `eval:true`, and report warnings if we cannot tell what the value is
### Why?
Libraries like jsPDF create Worker threads by passing inline JavaScript code as the first argument along with `{ eval: true }` as the second argument. Turbopack was incorrectly treating this inline code as a file path and attempting to resolve it as a module reference, causing build failures.
### How?
Added logic to detect when the `eval: true` option is passed to the Worker constructor. When this option is present, Turbopack now skips creating a worker reference since the first argument contains executable code rather than a file path. Added comprehensive test coverage using jsPDF to verify the fix works correctly.
Fixes#91642
The depth-based instant validation previously only iterated
URL-consuming segments as potential shared/new boundaries. Route groups
between URL segments were invisible — when a route group layout with
Suspense was shared in a client navigation, its Suspense appeared to
cover blocking code in the validation render even though it wouldn't in
reality.
This adds a second dimension to the validation iteration: group depth.
At each URL depth, we now also step through route group boundaries. A
new `discoverValidationDepths` function walks the LoaderTree and returns
an array where each entry represents the max group depth at that URL
depth. The validation loop iterates from deepest group depth to
shallowest within each URL depth, stopping on the first error.
Key design decisions:
- URL depth and group depth counters are advanced before the boundary
check, making the boundary condition a simple `nextUrlDepth > depth &&
currentGroupDepth >= groupDepth`
- The synthetic `(slot)` segment that Next.js inserts for parallel slots
is excluded from group depth counting since it doesn't represent a real
navigation boundary
- Group depths are discovered from the LoaderTree rather than the URL
pathname, making the tree the source of truth for validation bounds
This improves the blocked-request warning so it names the actual dev
resource being requested and gives clearer guidance on how to allow it.
When the source host is known, the message includes an inline
`allowedDevOrigins` config snippet; when the source is missing or
opaque, it explains why Next.js cannot infer a host to allow.
This defaults to `true`: https://doc.rust-lang.org/cargo/reference/profiles.html#debug
Nobody on the team uses a debugger often enough for this to be worth the time tradeoff: https://kobzol.github.io/rust/rustc/2025/05/20/disable-debuginfo-to-improve-rust-compile-times.html
I'm gathering some rough numbers, and I'll add them to the PR.
After `cargo clean` and one `cargo build -p next-napi-bindings` with each profile (Linux arm64):
```
du -sh target/*
4.0K target/CACHEDIR.TAG
7.9G target/debug
14G target/dev-with-debug
7.5M target/generated
```
This is nearly half the size.
Due to cargo quirks, the `dev` profile defaults to the `target/debug` directory. Of course, any disk size wins you get are more than offset if you need to use a debugger and end up with an additional profile.
```
hyperfine -w 1 -r 5 -p 'cargo clean' 'cargo build --profile dev-with-debug -p next-napi-bindings' 'cargo build -p next-napi-bindings'
```
...pending...
(this is on https://github.com/bgw/benchmark-scripts/, on my M2 Macbook inside of a Linux VM this takes closer to 2m20s)
- More profiles means more bloat to the `target` directory and more cold builds.
- There's no practical use case where we'd be okay with the performance hit of `debug-assertions` and not okay with the performance hit of `lto = false`. Neither profile was suitable for benchmarking.
- `release-with-assertions-no-lto` is too long to type and was giving me carpal tunnel.
This removes the warn-only default behavior and enforces the dev-origin guard by default. Cross-origin requests to internal dev resources now block unless they match the built-in local allowlist or an explicit `allowedDevOrigins` entry. The tests are expanded to cover default blocking, configured-but-not-allowlisted hosts, missing Referer in the no-cors path, and same-site requests without an Origin, and the docs are updated to match the new behavior.
## Move `adapterPath` from experimental to stable
Graduates `adapterPath` from `experimental.adapterPath` to a top-level
`NextConfig` option, following the same pattern used for
`cacheHandlers`, `cacheComponents`, `typedRoutes`, and other options
that moved out of experimental.
### Changes
- **Type definition** (`config-shared.ts`): Added `adapterPath` to
`NextConfig` interface, marked `ExperimentalConfig.adapterPath` as
`@deprecated`, moved default value to top-level in `defaultConfig`,
updated `NextConfigRuntime` and `getNextConfigRuntime`
- **Zod schema** (`config-schema.ts`): Added `adapterPath` to top-level
`configSchema`
- **Backward compat** (`config.ts`): Added
`warnOptionHasBeenMovedOutOfExperimental` call so existing
`experimental.adapterPath` configs still work with a deprecation
warning, updated `applyModifyConfig` to read from `config.adapterPath`
- **Runtime references**: Updated `build/index.ts`, `app-page.ts`,
`app-route.ts`, `pages-handler.ts` to use `config.adapterPath` /
`nextConfig.adapterPath`
- **Tests**: Updated 4 test fixture `next.config` files
- **Docs**: Updated app router doc (removed `version: experimental`
frontmatter, top-level config example), pages router doc title, and v16
upgrade guide
### What?
Remove the `TURBOPACK_STATS` feature that generated webpack-compatible
`webpack-stats.json` files from Turbopack's output.
**Deleted:**
- `crates/next-api/src/webpack_stats.rs` — the entire 305-line Rust
implementation that built webpack-format stats (assets, chunks, modules,
entrypoints) from Turbopack's module graph
**Rust changes:**
- `crates/next-api/src/lib.rs` — removed `mod webpack_stats;`
- `crates/next-api/src/project.rs` — removed
`should_create_webpack_stats()` which read the `TURBOPACK_STATS` env var
- `crates/next-api/src/pages.rs` — removed `generate_webpack_stats`
import and stats output emission
- `crates/next-api/src/app.rs` — same
**TypeScript changes:**
- `packages/next/src/shared/lib/constants.ts` — removed `WEBPACK_STATS =
'webpack-stats.json'` constant
- `packages/next/src/shared/lib/turbopack/manifest-loader.ts` — removed
`loadWebpackStats()`, `writeWebpackStats()`, `mergeWebpackStats()`, the
`webpackStats` field, webpack type imports, and the `TURBOPACK_STATS`
env guard in `writeManifests()`
- `packages/next/src/server/dev/turbopack-utils.ts` — removed
`shouldCreateWebpackStats` flag and two `loadWebpackStats()` call sites
- `packages/next/src/build/handle-entrypoints.ts` — same
### Why?
This feature is no longer needed and adds dead complexity. It was opt-in
via `TURBOPACK_STATS=1` but nothing in the current codebase or tooling
depends on it.
### How?
Pure deletion — no behavior change for any existing user, as the feature
was opt-in via an environment variable that is no longer checked.
**Intentionally left untouched:**
- Webpack's own `stats.json` generation (non-Turbopack path)
- The Turbopack bundle analyzer / `analyze` feature
- `route-bundle-stats.json` in `.next/diagnostics/`
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This PR makes configured `allowedDevOrigins` apply to cross-site no-cors dev asset requests. When browsers omit Origin for subresource loads, the dev guard now falls back to `Referer` so explicit allowlisted hosts can load `/_next/*` resources in development.
Previously, when `allowedDevOrigins` was configured, cross-site no-cors requests to internal Next.js dev resources were still blocked even for allowlisted hosts, because that code path never consulted the allowlist.
### What?
Removes the `experimental.devCacheControlNoCache` config option entirely
and hard-codes `no-cache, must-revalidate` as the dev server
`Cache-Control` header value.
Previously the option controlled whether the dev server responded with:
- `no-store, must-revalidate` (default, `false`)
- `no-cache, must-revalidate` (opt-in, `true`)
This PR first flips the default to `true`, then removes the option
altogether — making `no-cache, must-revalidate` unconditional in all dev
code paths.
### Why?
`no-cache` is strictly better than `no-store` for the dev server:
- `no-cache` allows the browser to revalidate (conditional
`If-None-Match`/`If-Modified-Since` requests), letting the server
respond with `304 Not Modified` when nothing changed → faster page loads
during development.
- `no-store` forces a full re-fetch every time, discarding valid cached
responses.
Since `no-cache` is the correct behavior for all dev users, the toggle
has no remaining value and can be removed to simplify the codebase.
### How?
**Two commits:**
1. `bcec825` — Flip the default from `false` → `true`; update
tests/fixtures/manifest to reflect the new default.
2. `e6e919f` — Remove the option entirely:
- Deleted `devCacheControlNoCache?: boolean` from `ExperimentalConfig`
interface, `config-schema.ts` Zod schema, `defaultConfig`,
`NextConfigRuntime`, and `getNextConfigRuntime()`.
- Replaced all four conditional ternaries in `base-server.ts`,
`router-server.ts`, `pages-handler.ts`, and `app-page.ts` with the
hard-coded string `'no-cache, must-revalidate'`.
- Deleted the `dev-cache-control-no-cache-disabled` test suite (was
testing the `false` path which no longer exists).
- Simplified the remaining `dev-cache-control-no-cache` test (removed
experimental framing).
- Synced `rspack-dev-tests-manifest.json`.
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Copied from https://turbopack-rust-docs.vercel.sh/turbopack/chunking.html
I had claude review it for correctness against the code. It found a few things where the names weren't correct or a trait was actually a struct, and it grouped things into sections. It also added some more details to a few sections, like the dev vs production chunking. I think this is a reasonable improvement.
I did a second pass with claude where I had it remove things it 1:1 duplicated with the existing rustdocs, and I had it add intradoc links.
Rendered:

**Previous:**
1. https://github.com/vercel/next.js/pull/91487
2. https://github.com/vercel/next.js/pull/91488
**Current:**
3. https://github.com/vercel/next.js/pull/89297
---
Most of the vary params infrastructure was already implemented in
previous PRs for static prerenders. This wires up the remaining pieces
for runtime prefetches — creating the accumulator, setting it on the
prerender store, and resolving it before abort — and adds additional
test cases covering empty/full vary sets, searchParams, metadata, and
per-segment layout/page splits with runtime prefetching.
**Previous:**
1. https://github.com/vercel/next.js/pull/91487
**Current:**
2. https://github.com/vercel/next.js/pull/91488
**Up next:**
3. https://github.com/vercel/next.js/pull/89297
---
When a prefetch response includes vary params, the segment cache rekeys
entries to a more generic path based on which params the segment
actually depends on. Previously, the rekeying only happened when vary
params were provided. Now that vary params are tracked for more response
types (and eventually will always be tracked), entries are rekeyed in
more cases than before.
This exposed a potential race condition: the scheduler would capture a
vary path at scheduling time and upsert the entry at that path when the
fetch completed. But the fetch functions themselves rekey entries to a
different (more generic) path upon fulfillment. The deferred upsert
could then move the entry back to the less generic path, undoing the
rekeying.
To fix this, move the upsert logic inline into the fetch functions that
fulfill entries, rather than deferring it to an external callback. This
removes the race condition, simplifies the model, and reduces
implementation complexity. The previous structure existed to avoid the
rekeying cost when vary params weren't available, but rekeying is
inexpensive and not worth the added indirection.
The upsert function itself already handles concurrent writes by
comparing fetch strategies and checking whether the new entry provides
more complete data than any existing entry. So it's safe to always call
it inline — whichever entry wins will be the most complete one.
**Current:**
1. https://github.com/vercel/next.js/pull/91487
**Up next:**
2. https://github.com/vercel/next.js/pull/91488
3. https://github.com/vercel/next.js/pull/89297
---
Prefetch responses include metadata (in the Flight stream sense, not
HTML document metadata) that describes properties of the overall
response — things like the stale time and the set of params that were
accessed during rendering. Conceptually these are like late HTTP
headers: information that's only known once the response is complete.
Since we can't rely on actual HTTP late headers being supported
everywhere, we encode this metadata in the body of the Flight response.
The mechanism works by including an unresolved thenable in the Flight
payload, then resolving it just before closing the stream. On the
client, after the stream is fully received, we unwrap the thenable
synchronously. This synchronous unwrap relies on the assumption that the
server resolved the thenable before closing the stream.
The server already buffers prefetch responses before sending them, so
the resolved thenable data is always present in the response. However,
HTTP chunking in the browser layer can introduce taskiness when
processing the response, which could prevent Flight from decoding the
full payload synchronously. The existing code includes fallback behavior
for this case (e.g. treating the vary params as unknown), so this
doesn't fix a semantic issue — it strengthens the guarantee so that the
fallback path is never reached.
To do this, we buffer the full response on the client and concatenate it
into a single chunk before passing it to Flight. A single chunk is
necessary because Flight's `processBinaryChunk` processes all rows
synchronously within one call. Multiple chunks would not be sufficient
even if pre-enqueued: the `await` continuation from
`createFromReadableStream` can interleave between chunks, causing
promise value rows to be processed after the root model initializes,
which leaves thenables in a pending state. Since the server already
buffers these responses and they complete during a prefetch (not during
a navigation), this is not a performance consideration.
Full (dynamic) prefetches are not affected by this change. These are
streaming responses — even though they are cached, they are a special
case where dynamic data is treated as if it were cached. They don't need
to be buffered on either the server or the client the way normal cached
responses are.
Follow-up to #91189 which added support for accessing root params inside
`generateStaticParams`. Previously, if a `generateStaticParams` function
tried to read a root param that hasn't been defined yet — either because
the current segment is supposed to define it, or because a child segment
defines it — `getRootParam` would silently return `undefined`. This is
especially problematic because #91019 will type root param getters as
`Promise<string>` without `undefined`, making this a silent type safety
violation.
This change makes `getRootParam` throw an explicit error with the
`generate-static-params` work unit store when the requested param is not
present in `rootParams`.