Backports #97128 ("Fix: Optimistic routing bugs leading to repeated
prefetch loops") to `next-16-3`.
Fixes an infinite prefetch loop in optimistic routing. A proxy that
rewrites every URL to inject a leading path segment — i18n with the
default locale hidden from the URL, so `/one/two` is served as
`/en/one/two` — combined with a fully dynamic target route like
`/[locale]/[...pages]`, produces a prefetch that never settles. When a
prefetch response came back, the client never checked whether the
rendered tree matched the one it had requested. When the params baked
into the request were wrong because of the rewrite, the task kept
re-attempting to fulfill the missing data and looped forever.
The prefetch path now records a dynamic-rewrite marker on the local
route definition when the response's tree diverges from the requested
one, which disables further optimistic resolution of that route and
invalidates the entries derived from it. This is the same strategy
normal navigation responses already used, now applied to the prefetch
path.
A second, related bug is also fixed: parallel routes with conflicting
dynamic params at the same level (`@modal/[...catchAll]` next to
`[username]`) can't be distinguished by the traversal algorithm, which
assumes each segment resolves independently without inspecting sibling
branches. When that scenario is detected, optimistic routing is now
disabled for the conflicting route rather than binding one branch's URL
parts to another branch's params.
### Verification on this branch
Cherry-picked from `5942b37a42`. Three source files conflicted, all
because `next-16-3` predates the post-16.3.0 refactor that split the
client's response decoder into its own module:
- `segment-cache/decode-server-response.ts` — this file does not exist
on this branch; it was extracted out of `navigation.ts` after 16.3.0.
Its change (tracking whether the decoded tree's segment identities
diverge from the base request tree) was ported into `navigation.ts`,
where the equivalent logic still lives. Canary decodes a compact
transport tree and compares segment identities inline during the walk;
this branch instead merges the response patch onto the base
`FlightRouterState` before converting it to a `RouteTree`. The check is
therefore re-expressed as a read-only walk in
`convertServerPatchToFullTree` that compares each server patch against
the base tree it is applied to, following canary's descent
(`convertServerPatchToFullTreeImpl`) exactly and using the same
comparison rules: segment identity via `matchSegment`, page segments
matched modulo embedded search params, server-filled `__DEFAULT__`
segments ignored, and inactive parallel-route branches (those carrying a
refresh state in the base) skipped. The result is threaded out through
`NavigationSeed.treeDivergedFromBase`.
- `segment-cache/cache.ts` — the consuming block in
`fetchSegmentPrefetchesUsingDynamicRequest` (mark-as-dynamic-rewrite,
invalidate derived entries, reject with immediate expiry) is
byte-identical to canary. The surrounding conflicts were only
incidental: the `RouteTreeAccumulator` type gaining
`treeDivergedFromBase`, and canary's transport-decoder call vs. this
branch's `convertRootFlightRouterStateToRouteTree`. Resolved by keeping
this branch's calls and adding the new field to the accumulators.
Canary's inline `NavigationSeed` construction in
`writeDynamicTreeResponseIntoCache` does not apply here because that
function delegates to `convertServerPatchToFullTree`, which now carries
the field.
- `router-reducer/create-initial-router-state.ts` — a one-line
accumulator initializer; kept this branch's
`convertRootFlightRouterStateToRouteTree` call and added
`treeDivergedFromBase: false`.
The other touched source files applied cleanly and are byte-identical to
canary: `segment-cache/optimistic-routes.ts` (the parallel-route
conflict detection) and `route-params.ts` (exporting
`canonicalizeURLPart`), along with `ppr-navigations.ts`, the small
cleanly-applied hunk in `navigation.ts`, and every test fixture.
`tsc` passes on the `next` package for all files touched here.
Behavioral verification is left to CI on this branch; the change carries
the `proxy-prefix-rewrite-prefetch-loop` and
`modal-catchall-sibling-dynamic-prefetch-loop` end-to-end suites plus
additions to the `optimistic-routing` suite.
Opened as a draft because the `decode-server-response.ts` →
`navigation.ts` port is a manual re-implementation across the refactor
boundary rather than a mechanical cherry-pick. The divergence walk in
`convertServerPatchToFullTree` is the part most worth a close review.
<!-- NEXT_JS_LLM -->
Backports #97190 ("[test] Compile the middleware redirect routes up
front in dev") to `next-16-3`.
<!-- NEXT_JS_LLM -->
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
Backports #96936 ("[refactor] Rename `encodeCacheTag` to
`encodeHeaderSafe`") and #96937 ("Encode the cache item name built by
`unstable_cache`") to `next-16-3`.
Fixes#76286. A cache implementation may serialize cache metadata into
HTTP request headers, whose values are limited to Latin-1.
`unstable_cache` assembles a cache item name from the request URL and
the name of the cached callback, and neither part was encoded. When that
name holds a character above U+00FF the conversion throws before the
request is dispatched, so the read never reaches the cache and the write
that follows it fails the same way. Nothing is stored, nothing is found,
and the entry falls back to the origin on every render.
The reachable case is a non-ASCII query parameter on any dynamic route
that calls `unstable_cache`, whether or not the route reads
`searchParams`, because `URLSearchParams` returns decoded keys and
values while the pathname stays percent-encoded.
The rename is folded in because it is the prerequisite: #96937 imports
`encodeHeaderSafe` from `server/lib/encode-header-safe`, which the
rename creates. It carries no behavior change, the function body is
identical apart from the parameter name, and the two were stacked in
this same order on canary.
### Verification on this branch
Cherry-picked from `1f0cd93b83` and `4286a42e2c` in that order, with no
conflicts.
`packages/next/src/server/web/spec-extension/unstable-cache.ts` is not
byte-identical to canary, because the release branch predates the
unrelated `willConsumerServerCache` change and keeps
`workStore.isStaticGeneration` at those two call sites. The diff this
backport applies to that file is exactly the encoding change and nothing
else. Every other source file is byte-identical to canary, and no
reference to the old `encodeCacheTag` name remains.
Behavioral verification is left to CI on this branch; the change carries
the `non-ascii-cache-item-name` end-to-end suite, which exercises a real
cache handler.
<!-- NEXT_JS_LLM -->
Backports #96726 ("Discard only cache entries that predate a tag
revalidation") and #96727 ("Reuse completed cache entries for the rest
of a request") to `next-16-3`.
Calling `updateTag()` in a server action made every later read of a
cache carrying that tag regenerate for the remainder of the request,
including reads of an entry that had just been generated after the
invalidation and therefore already reflected it. Two sequential reads of
the same cache function during the re-render produced two different
values within a single render, and each one repeated the work.
`isRecentlyRevalidatedTag` only asked whether a tag appeared in
`pendingRevalidatedTags`, and that array lives for the whole
`WorkStore`, which spans a server action and the render that follows it.
Each pending revalidated tag now records a `revalidatedAt` timestamp
taken from the same clock as `CacheEntry.timestamp`, and the renamed
`isRevalidatedAfter` reports an entry as stale only when the
revalidation is newer than the entry.
The second change fixes a `'use cache: private'` function executing its
body twice when it is called twice in one request, which defeats the
preload-then-read shape that motivates a preload in the first place. The
intra-request dedupe map dropped an entry as soon as its fill completed,
so it only ever covered concurrent invocations, and private caches have
no cache handler in production to fall back to. Completed invocations
now move into `completedCacheInvocations` on the work store instead of
being dropped.
The two are backported together because #96727 builds on the discard
checks that #96726 restructures. They were stacked in this order on
canary, and #96727 conflicts in
`test/e2e/app-dir/use-cache/use-cache.test.ts` when applied alone.
### Verification on this branch
Cherry-picked from `5da1c1ae03` and `1ff6a80358` in that order, with no
conflicts. All source files are byte-identical to canary. Behavioral
verification is left to CI on this branch; the changes carry the
`action-dedupe`, `dedup-sequential` and `private-dedup-sequential`
fixtures in the `use-cache` suite, and a handler-call count assertion in
`use-cache-custom-handler`.
<!-- NEXT_JS_LLM -->
Backports #97181 ("Allow literal exports in `'use cache'` files") to
`next-16-3`.
A `'use cache'` page or layout that exports a route segment config
literal, such as `export const instant = false`, `export const
dynamicParams = true`, `export const prefetch = 'partial'`, or `export
const maxDuration = 5`, fails the build with "Only async functions are
allowed to be exported in a "use cache" file".
Rejecting exported literals is meaningful only in a `'use server'` file,
where every export becomes a server reference and a runtime check
asserts that each one is a function. A `'use cache'` file wraps only
exports that are, or might be, functions, so known non-function values
can pass through. The check is now scoped to `in_action_file`.
### Verification on this branch
Cherry-picked from `c0c64a0ab1` with no conflicts. All source files are
byte-identical to canary. This touches `crates/next-custom-transforms`,
so the release build needs the native binary rebuilt. Behavioral
verification is left to CI on this branch; the change carries transform
fixtures on both the server and client graph.
<!-- NEXT_JS_LLM -->
Backports #97166 ("Restore the live `headers()` view of the incoming
request") to `next-16-3`.
Fixes#97049. This is a regression introduced in 16.3.0 itself. #94703
and #95116 are both in the `v16.3.0` tag, and between them they left
`headers()` detached from the request it describes. A Proxy that writes
a header onto `request.headers` and then reads it back through
`headers()` gets the value from before the write, while reading the same
header directly off `request.headers` returns the new value, so two APIs
describing the same request disagree. Whether the write is seen at all
depends on ordering, because the view is built on first access.
`HeadersAdapter.seal` now accepts a set of header names to omit from
every read operation, and `getHeaders` seals the shared request headers
directly instead of stripping them from a copy. Both earlier intentions
survive: the internal headers stay on the request where the framework
still reads them, they stay invisible to userland `headers()`, and the
sealed view tracks the request again.
### Verification on this branch
Cherry-picked from `3c97df56ea` with no conflicts. All source files are
byte-identical to canary. Behavioral verification is left to CI on this
branch; the change carries unit coverage in `headers.test.ts` and the
`proxy-headers-live-view` end-to-end suite.
<!-- NEXT_JS_LLM -->
Co-authored-by: joaquin <tachsinachmet@gmail.com>
Backports #96600 ([turbopack] Add `turbopack_ecmascript` and
`turbopack_wasm`'s embeded FS to `internal_assets_conditions`) to
`next-16-3`.
<!-- NEXT_JS_LLM -->
Co-authored-by: Sam Poder <sampoder@berkeley.edu>
Previously, adding a truthy `instant` config to a route segment (e.g.
`export const instant = true`) did two things: it enabled instant
navigation validation, and it implicitly opted the route into Partial
Prefetching.
Now those concerns are fully separate. `instant` only controls
validation, so enabling it no longer silently changes how a route is
prefetched. Partial Prefetching is enabled with the `prefetch` segment
config (`'partial'` or `'unstable_eager'`), or globally with the
`partialPrefetching` option in next.config.
For routes whose only opt-in was a truthy `instant`, `<Link
prefetch={true}>` goes back to performing a legacy full prefetch instead
of being downgraded to a partial one. `instant = false` still opts a
route out of the dev-time `prefetch={true}` warning.
Adds a regression test asserting that a route that only exports `instant
= true` keeps the full-prefetch behavior.
## Summary
Enable the project-local TypeScript CLI checker by default during `next
build`, while preserving `experimental.useTypeScriptCli: false` as an
opt-out to the TypeScript compiler API. Update the TypeScript 7
guidance, diagnostics, tests, and documentation to match the new
default.
## Verification
- `pnpm exec jest --runTestsByPath test/unit/isolated/config.test.ts`
- Pre-commit lint-staged checks passed (Prettier and ESLint)
- Not run: `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts` (isolated
fixture dependency installation was blocked by unavailable npm registry
access)
- Not run: `pnpm --filter=next types` (repository has pre-existing
unrelated TypeScript errors)
<!-- NEXT_JS_LLM -->
## Summary
The `pages-router-app-not-found` test verifies behavior implemented by
the Next.js adapter. The legacy `@vercel/next` builder has a
pre-existing behavior where it prefers the Pages Router error page for
this hybrid routing case.
Skip this test when deployment tests run without the adapter. It will
continue running in development, `next start`, and adapter deployment
tests.
## Verification
- `NEXT_TEST_MODE=deploy NEXT_ENABLE_ADAPTER=0 pnpm testheadless
test/e2e/app-dir/pages-router-app-not-found/pages-router-app-not-found.test.ts`
- `pnpm testheadless
test/e2e/app-dir/pages-router-app-not-found/pages-router-app-not-found.test.ts`
## Summary
- Treat the root `scroll-padding-top` as the lower boundary of the
usable viewport during App Router navigations, so content obscured by a
sticky header is not incorrectly considered visible.
- Preserve the merged Fragment scroll-ownership state machine: empty
Fragments remain unavailable targets, while real targets are classified
against the padding-aware viewport.
- Resolve pixel and percentage values and use the same visibility rule
in both the legacy element handler and the Fragment-ref handler.
- Add regression coverage showing that Next scrolls when the destination
is hidden inside the padding boundary, while preserving the current
scroll position when the destination is genuinely visible below it.
## Performance
The root computed style is resolved lazily, only after the candidate
produces client rects. Empty Fragments and hash navigations do not
perform this lookup. For a real route-scroll target, the resolved value
is cached locally and reused by the second geometry check after
`scrollTop = 0`.
This does not add work to scroll events or every render, and keeping the
value local allows responsive CSS, root classes, and custom properties
to change between navigations. In a 50-iteration dirty-style Chromium
probe, both the existing and updated paths reported 50 style
recalculations and 50 layouts, indicating that the lookup reused the
style/layout update already required by the handler in that test.
## Related work
#96342 is now merged into `canary` and handles empty Fragment scroll
ownership. This PR composes with its `NoClientRects` / `InViewport` /
`OutOfViewport` state machine by changing only the visible-region
boundary for real targets.
## Verification
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=false pnpm
test-dev-turbo
test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts -t "scroll
padding"` (3/3)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/navigation-focus/navigation-focus.test.ts` (5/5)
- Manually compared the same sticky-header reproduction before and after
the change.
<!-- NEXT_JS_LLM -->
Closes#96339
Caches that started filling after a prerender was aborted would produce
an empty stream, because an aborted `renderSignal` made [the signal we
pass to `prerender()` inside `use-cache-wrapper`] be aborted
immediately. So we produced an empty entry and saved it, essentially
poisoning the cache.
Fixed by removing `renderSignal` from the `AbortSignal.any(...)` we pass
in and returning a rejected promise before we get to the cache filling
codepath. This should not be observable to userspace because the
prerender is already aborted.
## Summary
Enable `experimental.turbopackFileSystemCacheForBuild` by default in all
environments, including generic CI. Explicitly setting the option to
`false` remains the opt-out. Update the focused coverage and
documentation to match.
## Verification
- `IS_TURBOPACK_TEST=1 pnpm test-start-turbo
test/e2e/filesystem-cache/build-cache-default.test.ts`
- `pnpm prettier --with-node-modules --ignore-path .prettierignore
--check packages/next/src/server/config-shared.ts
test/e2e/filesystem-cache/build-cache-default.test.ts
docs/01-app/03-api-reference/08-turbopack.mdx
docs/01-app/03-api-reference/05-config/01-next-config-js/turbopackFileSystemCache.mdx`
- Not run: `pnpm --filter=next build` (the checkout has 11 pre-existing
TypeScript errors in unrelated files)
<!-- NEXT_JS_LLM -->
## Summary
When Pages Router and App Router routes coexist, a Pages Router route
that returns `notFound` will internally hand rendering to the App Router
not-found page. Next.js [prefers the App Router `/_not-found` entry for
404
responses](https://github.com/vercel/next.js/blob/5f5e4773bbb54f19697063ea093e8d89ca7d081a/packages/next/src/server/base-server.ts#L2965-L2980)
before falling back to a Pages Router `/404` page.
When deployed via an adapter, this means it would have attempted to
resume the Pages entry, which wouldn't contain the relevant prerendered
shell or postponed state. If the not-found page suspends, its response
cannot be resumed using the metadata associated with the Pages Router
output.
For adapter requests, this marks the handoff with an empty postponed
state. This uses the existing resume protocol to perform a complete,
non-cacheable dynamic render. `next start` continues to use the normal
PPR path, where it can load and resume the App Router not-found output
directly.
This trades PPR shell reuse for correctness only during this
cross-router not-found handoff. Longer term, adapters should have an
explicit contract for cross-output handoffs so they can select and
resume the App Router not-found output normally, without necessarily
declaring the pages router output PPR-capable.
The change also documents the empty postponed-state contract and adds
deployment regression coverage.
## Verification
- Added an end-to-end test that verifies a Pages Router `notFound`
response includes the dynamic App Router not-found content when
deployed.
- `pnpm testheadless test/e2e/app-dir/pages-router-app-not-found`
- `pnpm --filter=next types`
<!-- NEXT_JS_LLM -->
## Summary
[PR #96354](https://github.com/vercel/next.js/pull/96354) changed
`retry()` to enforce its duration as a wall-clock budget instead of
effectively treating it as a retry count. This exposed the test’s
previous two-second budget, which did not reliably cover the intentional
one-second `after()` delay plus deployment and ISR latency.
Separates the callback delay from the ISR settling timeout and waits for
timestamp reads to stabilize before establishing a baseline.
<!-- NEXT_JS_LLM -->
### What?
Makes `experimental.turbopackFileSystemCacheForBuild` (Turbopack's
on-disk
filesystem cache for `next build`) enabled by default, matching the
dev-mode
flag. It stays disabled by default in non-Vercel CI environments, and
can
always be turned off explicitly with
`experimental.turbopackFileSystemCacheForBuild: false`.
### Why?
The build filesystem cache greatly speeds up warm builds, but was
previously
opt-in (only auto-enabled for canary builds on the Vercel builder).
Defaulting
it on gives everyone faster subsequent builds, while keeping it off in
non-Vercel CI where the cache is unlikely to persist between builds.
### How?
- `turbopackFileSystemCacheForBuildDefault()` now returns `true` except
when
`isCI && !NOW_BUILDER` (removed the `isStableBuild()` gate).
- The explicit `false` opt-out already works via the existing
`config.experimental?.turbopackFileSystemCacheForBuild || false` read.
- Adds an e2e test (`test/e2e/app-dir/turbopack-fs-cache-build-default`)
asserting `.next/cache/turbopack` is written by default, empty when the
flag
is `false`, and empty in non-Vercel CI.
- Updates the `turbopackFileSystemCache` / Turbopack docs.
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: vercel-fleet[bot] <308483924+vercel-fleet[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
An unhandled rejection was previously logged by up to three independent
process listeners at once: the render runtime's crash-prevention handler
in `process-error-handlers.ts` (a bare `console.error`), the router
server's `Log.error('unhandledRejection: ', err)`, and the dev server's
`logErrorWithOriginalStack`. The runtime handler must exist on every
deployment target (#77997), but on self-hosted `next start`/`next dev`
it shares a process with the router server's and dev server's listeners,
so a single rejection was logged multiple times in different formats.
THe first commits adds a test showing the current behavior where we log
multiple times.
The second commit introduces `registerUnhandledRejectionListener` and
`isUnhandledRejectionListenerRegistered` in `process-error-handlers.ts`,
and converts the router server and dev server to check-then-register
instead of installing their own rejection loggers:
- The listener function is shared via a `Symbol.for` key on
`globalThis`, so multiple copies of the module (e.g. in the pre-compiled
server bundle and in a route module bundle) register and detect a single
listener instance.
- The registration check queries
`process.listeners('unhandledRejection')` instead of a module-global
flag, so it stays accurate even after external code calls
`process.removeAllListeners`. `installProcessErrorHandlers` therefore
calls the register function unconditionally.
The `uncaughtException` handlers are left as they are; they have the
same duplication and could be consolidated the same way in a follow-up.
## Summary
Fix the build output for App Router `not-found` routes that get
partially prerendered when cacheComponents is enabled.
Previously, Next.js treated any statically generated `/_not-found` route
as a complete static 404, even when it contained postponed state. It
copied the partial HTML shell to `server/pages/404.html` and omitted the
corresponding `/_not-found` prerender from adapter outputs.
This change distinguishes fully static not-found routes from partially
prerendered ones. Fully static routes retain the existing `404.html`
behavior, while PPR routes remain resumable prerender outputs.
This allows a `not-found.tsx` that suspends under Cache Components to
render its dynamic content when deployed.
Closes https://github.com/vercel/next.js/pull/96399
## Testing
Expanded `not-found-non-document-dynamic` coverage to verify that:
- dynamic not-found content renders for ordinary missing paths
- dynamic not-found content renders when `/_not-found` is selected by a
rewrite
- a PPR not-found does not emit `server/pages/404.html`
- its adapter output includes `PARTIALLY_STATIC` metadata, postponed
state, and a resume chain
Also verified that fully static App Router not-found routes continue to
emit and serve `pages/404.html`.
- `__NEXT_CACHE_COMPONENTS=true pnpm test-start-turbo
test/e2e/app-dir/not-found-non-document-dynamic/not-found-non-document-dynamic.test.ts`
- `pnpm test-start-turbo test/e2e/app-dir/not-found/basic/index.test.ts`
When a static fallback shell is served as-is — e.g. from a CDN in a
deployed environment — the client can't hydrate from inline Flight data;
it fetches the full RSC payload separately and reconstructs the initial
payload on the client. During that reconstruction, the head's partiality
flag was accidentally set to the head itself. Since the head is always
truthy, the head was unconditionally treated as partial.
This happens to be unobservable today: the only flows that reach the
reconstruction are fallback-shell prerenders, where the server
pessimistically marks the head partial anyway, and when Cache Components
is enabled the client ignores the per-payload flag entirely in favor of
response-level partiality. But we're about to rework the RSC response
format so that head partiality has a single, load-bearing source of
truth, so the flag needs to survive the reconstruction correctly rather
than by coincidence.
While auditing the payload consumers, I also fixed the payload shape
check in segment prefetch generation (it used `&&` where `||` was
intended, so it accepted any shape) and deleted
`should-hard-navigate.ts`, which no longer has any callers on the
client.
Adds e2e coverage asserting that metadata survives hydrating a fallback
shell page: the title is correct after hydration, the head is included
in prefetched data, and a return navigation is served entirely from the
cache that was populated during initial hydration. In deployed
environments this exercises the client resume path, which is where the
reconstruction runs.
## Summary
DOM-capable crawlers can consume streamed metadata, but with
cacheComponents enabled, it treated them like HTML-limited bots and
forced a full dynamic render. This meant capable crawlers could not use
the prerendered shell even when they were not matched by
`htmlLimitedBots`.
This change lets DOM-capable crawlers follow the normal PPR shell and
streaming-metadata path. `htmlLimitedBots` remains the source of truth
for requests that need blocking metadata, including when a custom
pattern explicitly matches a DOM-capable crawler.
## Test Plan
Tests were added that contained a Suspense boundary that intentionally
never resolves. The test reads the response stream until it receives the
fallback, then verifies that blocking metadata has already been emitted
while the unresolved page content has not. A fully buffered response
would never return, so this avoids relying on timing or arbitrary
delays.
This PR is stacked on #96366.
## Summary
Expand Cache Components metadata streaming coverage across the default,
custom, and wildcard `htmlLimitedBots` configurations.
The active matrix now covers regular user agents, configured
HTML-limited bots, built-in bot classifications, metadata placement for
static and dynamic routes, PPR shell behavior, streaming versus buffered
responses, and hydration after revalidation. It replaces the skipped
legacy PPR metadata suite with active Cache Components coverage.
## Summary
- treat Fragment instances without client rects as unavailable route
scroll targets
- leave the shared scroll signal unconsumed so another changed segment
can handle an empty route Fragment
- avoid focus and scroll side effects when an empty segment cannot claim
ownership
- consume and clear an explicit hash intent when no matching target
exists, without falling back to route content
- cover empty and visible intercepted routes across both scroll
handlers, including focus preservation and real hash scrolling
This PR is stacked on #96323, which provides the minimal reproduction.
## Verification
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/parallel-routes-scroll-owner/parallel-routes-scroll-owner.test.ts`
(8/8)
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/navigation-focus/navigation-focus.test.ts` (5/5)
<!-- NEXT_JS_LLM -->
## Summary
Adds a minimal App Router regression test for a soft navigation where an
empty parallel route is rendered after persistent page content.
The test runs with `appNewScrollHandler` both disabled and enabled. The
legacy-handler case passes normally, while the new-handler case uses
`it.failing` to highlight the regression.
Closes NAR-854
### TLDR
New approach to #94949. We can work around the problem of debug info
getting clamped when re-serializing by doing more work, which should be
good enough unless we can figure out the correct way to adjust the
clamping behavior in react.
### The problem
Before this PR, instant validation works like this:
1. deserialize the full page payload (in stages)
2. reserialize each segment (in stages)
3. reassemble the segments into a combined payload, varying the stage
per segment as needed
Unfortunately, in step 2, we lose timing debug info that tells us when
IO operations started/ended:
https://github.com/facebook/react/blob/c235eb40f1c3121798d233bf992fd8f8c8558d06/packages/react-server/src/ReactFlightServer.js#L5550-L5553
This means that even if we provide an appropriate `endTime` during step
3, no IO info will be omitted by `filterDebugInfo`, because it was all
re-timed to when the render occurred. In practice, this means that when
we report a blocking dynamic hole, it might point to a different await
than the one that was actually blocking in the stage that we rendered
the segment in.
### The solution
(h/t to @gnoff for coming up with this trick)
We can work around this problem with no changes in React. We just have
to apply an `endTime` when deserializing the payload for the first time,
before reserializing each segments. However, this means that we can no
longer disassemble the payload into segments in one pass -- e.g. if we
trim the debug info to what it'd be in the `Static` stage, we can't get
good data for the `Runtime` stage anymore. So we have to run the segment
disassembly separately for each stage we care about.
The debugInfo can now vary depending on the stage, we need to change the
shape of `SegmentCacheItem`. Previously, it was like this:
```
'/foo':
chunks:
Static: [...]
Runtime: [...]
Dynamic: [...]
debugChunks: [...]
```
But to account for the varying debug info, we now need to store the
partial chunks, the complete chunks, and the debug chunks separately for
each stage:
```
'/foo':
Static:
chunks: [...]
allChunks: [...]
debugChunks: [...]
Runtime:
chunks: [...]
allChunks: [...]
debugChunks: [...]
Dynamic:
...
```
Most of the changes in the PR come from adjusting the code to
- create separate entries for each segmentPath + stage in the new way
outlined above, using an `endTime` based on the stage
- consume the cache entry appropriately
## Summary
When PPR is enabled, requests matching a custom `htmlLimitedBots`
pattern could still use the prerendered route path. As a result, the
configured blocking-metadata behavior was not applied consistently.
Use the existing streaming-metadata decision when selecting the PPR
render path. Built-in bots retain their existing fully buffered
behavior, custom HTML-limited bots can continue streaming body content
after metadata resolves, and regular user agents remain unchanged.
## Verification
- HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/html-limited-bots-ppr/html-limited-bots-ppr.test.ts
- HEADLESS=true pnpm test-start-webpack
test/e2e/app-dir/html-limited-bots-ppr/html-limited-bots-ppr.test.ts
Co-authored-by: Sebastian Silbermann <sebastian.silbermann@vercel.com>
A HTTP request can render the same React tree in multiple passes with
different semantics for `connection()`: the prospective and final
prerenders of a runtime prefetch, or a navigation's dynamic render and
the runtime prerender that is spawned from it to refresh the client's
prefetch cache. All of these passes previously resolved `headers()` to
the single sealed headers object created lazily by the request store, so
userland caches keyed on the identity of `await headers()` (any
per-request memoization that treats the headers object as "the request")
leaked promises across passes.
With this change, every render pass resolves `headers()` to a distinct
object over the same underlying data, making "the headers object
identifies the request within one render pass" a real contract.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
How long do you expect this to wait at worst (before failing)?
```js
await retry(async () => {
expect(await browser.elementById('never-appears').text()).toBe('x')
}, 15_000)
```
After this PR, the answer is 15 seconds. (Well, technically, 15 seconds
+ whatever last attempt is, so 20.)
But before this PR, the answer was 3 minutes.
## What?
There's a bug (IMO) in `retry`. Instead of waiting for the time you give
it, it counts tries.
For example, `retry(fn, 15_000)` reads as "keep trying for 15 seconds".
However, what it actually was doing is divide 15000 by the 500ms
interval and try 30 times, sleeping 500ms in between every try.
This is a problem because **it wasn't measuring the stall time of each
try itself.**
When a try is cheap, it doesn't matter. But a try that looks for an
element in the browser isn't cheap: if the element isn't there, the
selector waits 5 seconds before giving up.
In the example above, each try sits in the selector for its full 5
seconds, and there are 31 of these intervals, plus the 15 seconds of
sleeping in between. This ends up waiting 170103ms, i.e. three minutes
for a wait that says 15 seconds.
Jest kills the test at its own timeout long before that, and the actual
error gets thrown away. What's left is `Exceeded timeout of 120000 ms`,
which doesn't say which assertion never came true. In the one I measured
it was `page.waitForSelector: Timeout 5000ms exceeded`. My agent lost an
hour trying to make sense of this twice now.
## Fix
We count time now. After the fix, the retry watches the clock and stops
when its time is up. It still always makes at least one try. The
interval no longer has to divide the duration, because nothing counts
steps any more. If the last try is slow, we're gonna wait for that last
try to end (so the timeout is still not *strictly* respected) so that we
can show the error in the correct test.
Cheap waits behave exactly as before. In my local run, a try that had to
repeat cost 251ms at the median, 459ms at the worst. So the only calls
this changes are ones that were already failing, and those now fail with
their own error instead of a test timeout. One flaky dev test that used
to die at 120 seconds with nothing to show now fails in 19.5 seconds
saying `Failed to retry within 15000ms (waited 16022ms)`, next to nine
tests that take 2.2 seconds each.
I added some regression tests. Since it's a test helper, a unit test
seemed fine.
## What this means for existing tests
The catch is that a call whose tries are slow now gets less time than it
used to, and this helper has 2110 call sites, so CI is the real check.
It found one file: the hash scroll tests in `navigation.test.ts` wait
with the default 3 seconds, and each try there costs 150-300ms on a
runner, so they were really getting about 5. They need more than 3, so
they failed on two dev shards. I gave them 10.
If this is green, we can merge it in isolation from the rest of the
stack.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
In #96074 I put the `partialFallback` behavior behind the
`partialPrefetching` flag, so that we don't risk an explosion in ISR
costs for apps that haven't opted into Partial Prefetching. But it turns
out I only handled this for deploy mode — i.e. the build output consumed
by the Vercel adapter. The behavior was still unconditionally on for
`next start`.
The reason it was easy to miss is that `next start` and the adapter
express partial fallback shells through completely separate mechanisms.
The adapter emits a `partialFallback` flag into the build output and
lets the platform's ISR layer perform the shell upgrade. `next start`
has no such flag; the server performs the upgrade itself, in the
compiled page runtime. So the gate I added to the adapter output had no
effect on the self-hosted path.
To understand how the `next start` path was configured, I looked at the
PR that removed the original top-level `partialFallbacks` config flag
(#93859). Before that PR, the behavior was gated in the runtime; when
the flag was removed those gates were deleted and the behavior became
always-on.
The important subtlety is that only the *upgrade* should be gated, not
the shell machinery as a whole. On `next start`, the value that decides
whether a shell can be specialized (`remainingPrerenderableParams`) also
drives core Cache Components behavior — build-time partial prerendering
of params and serving build-time sub-shells — which must stay on
regardless of `partialPrefetching`. So this gates only the two things
that make up the "upgrade on first request" cost risk: the background
ISR revalidation that specializes a shell per request, and the
client-facing `isFallbackUpgradeable` signal that tells the client to
retry a prefetch waiting for that upgrade.
Adds coverage with a `partialPrefetching`-disabled fixture asserting
that fallback shells stay shared rather than specializing per request.
Currently, importing a new module causes a complete eviction (`clear()`)
and re-evaluation of server chunks, both in the Turbopack runtime's
module cache and Node's `require.cache`.
Here's what happens today:
- A new module is imported into a chunk's graph
- This changes its `availability_info`, which in dev for non-entry
chunks is encoded into the chunk's filepath
- `VersionedContentMap` works entirely on chunk paths. When we construct
instructions to transition a chunk into its new state, we fail to find
the prior state and fall back to `clear()` as described above.
An ideal version of this is a refactor that implements
`VersionedContentMap` on a per-module basis, not a per-chunk one. This
PR doesn't do that, but achieves consistent module-level updates when a
chunk's availability info changes.
It does this by implementing chunk lists for server entry chunks the
same way the client hmr implementation does: an entry chunk's version
aggregates the versions of its dependent chunks (including dynamically
imported ones), keyed by merger rather than path. Entry chunks do not
encode availability info in their paths, so they are insulated from
missing versions. Updates are applied in Node.js through the same shared
merged-update machinery in the unified hmr runtime that the client
already uses, plus a new `ChunkListUpdate` branch in the server hmr
client to unwrap the merged updates.
Details:
- The merged-update wire format (`EcmascriptMergedUpdate` etc.) moves
from turbopack-browser into shared `turbopack_ecmascript::chunk_list`,
along with runtime-agnostic `ChunkListVersion` and `update_chunk_list`
implementations.
- `turbopack-nodejs` gains a chunk content merger mirroring the browser
one. `EcmascriptBuildNodeEntryChunk`'s versioned content is now a
chunk-list content over its sync and async chunks. It is version-only
and never emitted; the entry chunk still inlines its loader calls.
- The aggregate server hmr subscription now tracks only entry chunks.
Shared chunks under `server/chunks/` ride the entry's `ChunkListUpdate`
as module deltas, so their content-hash paths no longer matter.
- The Node hmr client applies `ChunkListUpdate` by feeding each nested
merged update through the shared apply path. Modules that appear in an
"added" chunk but already exist in the module cache were moved by a
chunk rename and are treated as modified.
- On a successful partial apply, the hot reloader clears the manifest
cache for updated chunks and notifies browsers to refetch RSC, without
clearing `require.cache`.
### Test Plan
Adds a series of e2e tests to catch the described manual test plan in
both entry and dynamic chunks.
This replaces per-chunk Server HMR turbo tasks with a single firehose
subscription that diffs every HMR chunk. This significantly cuts the
number of tokio task churn on projects with many server chunks and
centralizes the diff/clear logic. It leads to a multi-second saving in
both cold and warm builds in a large app.
This PR also renames `clear()` to `reEvaluateAllModulesExpensive()` to
label directly that this is a costly operation and should only be done
in exceptional circumstances.
A following PR will bring this to client chunks.
Rust:
- New `aggregate_hmr` module: `AggregateHmrVersion` keyed by chunk path,
`merged_partial_update` builder, and `is_hmr_eligible_chunk` (excludes
`.map` files, which would force every diff to `Total`).
- `Project::all_hmr_version_state` / `all_hmr_update` aggregate over the
whole `hmr_root_path`. The seed transition emits an empty `Partial` so
the JS consumer doesn't treat it as a restart and wipe handlers the
triggering request just populated. Any chunk requiring `Total`/`Missing`
escalates the batch.
- `VersionedContentMap::hmr_chunks_in_path` lists eligible chunks with
their `VersionedContent`.
NAPI:
- `projectAllHmrEvents(target)` returns a single subscription.
JS:
- `setupServerHmr` subscribes once via `allHmrEvents` instead of fanning
out over `hmrChunkNamesSubscribe`.
- `reEvaluateAllModulesExpensive()` evicts every chunk under
`server/chunks/` from `require.cache` directly rather than tracking
subscriptions. This is a bit fragile as it relies on the path prefix and
scanning require.cache.
Watching too many files can hurt performance, and impacts how much work
Turbopack is doing. See for example the conversation around
https://github.com/vercel/next.js/pull/94597.
This PR handles the following four cases:
- Git worktrees: we can include a guard to prevent exiting a worktree
(by looking at the `.git` file).
- Git repository boundaries: we can include a guard to prevent us from
exiting a checked-out repository and watching the repository’s parent
directory.
- We will log a warning if this happens.
- Empty / bogus `package.lock` files & `package-lock.json` files (and
equivalents using other package managers): if we spot a
`package-lock.json` file but it has no packages or a `package-lock.json`
w/o an associated `package.json` file. We can ignore them. This handles
the case of accidentally running `npm install` in any directory.
- Home directory detection: we can detect if we’re going to start
watching your home dir and rollback up if this is the case.
- We will log a warning if this happens.
- If your Next config is in your home dir, we will still run your app
there as Next config is the furthest we can rollback
- NPM, Yarn, Lerna & Bun workspaces: based on the `workspaces` field in
the `package.json` or `lerna.json`
I would recommend using split-view to review. I also cleaned up the
`warnDuplicatedLockFiles()` function because it was a bit unnecessary to
repeat the full message.
Closes https://github.com/vercel/next.js/issues/92978
## Summary
Client-handled Server Action redirects currently return a `303` without
a `Location` header and may include a streamed RSC response body. Some
HTTP intermediaries reject this response or strip its body because it
looks like an HTTP redirect.
Return `200` for client-handled Server Action redirects while continuing
to use `x-action-redirect` and the RSC response. Progressive-enhancement
form submissions still return a standards-compliant `303` with
`Location`.
Fixes#92882Fixes#74026
## Verification
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/actions/app-action.test.ts
test/e2e/app-dir/app-basepath/index.test.ts
test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts`
- `HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/actions/app-action.test.ts
test/e2e/app-dir/app-basepath/index.test.ts
test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts`
- `pnpm --filter=next types`
<!-- NEXT_JS_LLM -->
## Summary
- Preserve the original static worker request when generating the
Turbopack module-context lookup, instead of passing an already-rewritten
URL object.
- Adds an e2e fixture for bare-module Worker URLs with conditional
wildcard exports.
It looks like this might have regressed in #88602Fixes#94015Closes#94043
## Verification
- `cargo check -p turbopack-ecmascript`
- `HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/worker-module-url/worker-module-url.test.ts`
- `HEADLESS=true pnpm test-start-webpack
test/e2e/app-dir/worker-module-url/worker-module-url.test.ts`
We were missing some test coverage for image optimization. This PR adds
tests for many missing image formats.
Additionally, this PR also ensures the sharp encoders match that
allowlist from detectContentType() such that future versions of sharp
that enable new encoders by default are not automatically enabled in
next/image.
Practically, there is no change to the end user because
detectContentType() runs before sharp so that is the true allowlist, but
this gives us a little more control to align the sharp allowlist as well
(since sharp can release new features in semver minor).
Removes the "allow-runtime" prefetch config, and turns its behavior on
implicitly wherever Partial Prefetching is enabled.
The original motivation for "allow-runtime" was to give apps more
control over server costs triggered by prefetches. Until a route
explicitly opts in, prefetches would only be served from the CDN, not
from the server. The problem, though, was it was very confusing to know
when to add or remove this configuration. The incentive for many apps
was to add it everywhere, with no clear signal for when to remove it.
Our updated thinking is that Partial Prefetching itself already provides
sufficient protection against runaway prefetching costs: per-link
prefetches only happen on Link components that explicitly opt in with
the prefetch prop.
The optimizations landed earlier in this stack also make allow-runtime
less necessary: on pages where all the content is statically renderable,
prefetches are served from the static cache and no runtime request is
ever issued; only a page that accesses non-static data is prefetched at
runtime.
The upshot of this decision is that runtime versus static becomes an
internal optimization; the same content gets prefetched regardless of
whether or how Next.js is able to optimize it.
If we're reasonably confident that a segment can be prefetched
statically without omitting data that would have been included during a
runtime prefetch (e.g. cookies), the client should attempt prefetch the
segment statically instead of going straight to a runtime request.
The decision for whether to do this is based on the
ShouldAttemptStaticPrefetch added in previous steps.
If it turns out the static response is not sufficient, then it will fall
back to a runtime request.
This makes prefetching cheaper for pages that are fully statically
renderable. We can make the optimization better in the future with more
reliable per-segment computation of the ShouldAttemptStaticPrefetch, but
the current approach should at least work for fully static pages, which
is what's most important.
This optimization applies during both the Shell phase and the
Speculative phase of the prefetching algorithm.
Test-only.
The test assertion was racy: it asserted no with-query-param RSC request
during hash-only navigations, but a late prefetch of the
`#link-to-query-param` link makes exactly that request (failed 3/3 on a
prod CI shard).
Assert the actual invariants instead:
- hash-only navigations issue no navigation RSC requests at all
(prefetches, identified by their headers, are ignored)
- a query change *does* issue one — with `prefetch={false}` on the link
so it can't be served from the prefetch cache.
[`httpxy`](https://github.com/unjs/httpxy) is a faster and actively
maintained variant of `http-proxy`. It also doesn't need any manual
patches to avoid security vulnerabilities in Next.js.
`httpxy` is used by Nuxt and maintained by the Nitro team.
Adds a server-computed signal to static per-segment prefetch responses
that tells the client whether a runtime prefetch request would return
more content than the static response already contains.
The signal is computed by tracking whether the prerender accessed any
data source that hangs during a static prerender but would resolve
during a runtime prerender. For example: cookies, headers, fallback
params, and search params.
The information is encoded two places:
- As a prefetch hint called ShouldAttemptStaticPrefetch. This tells the
client that it's worth attempting to do a static prefetch instead of a
runtime one. It's only an optimization, though: if the static response
ends up being insufficient, the client will follow it up with a runtime
request. It's semantically OK if there are false positives.
- Embedded in the static segment response. This tells the client whether
the static response is missing data that would have been included in a
runtime response. Unlike the prefetch hint, this value must never
falsely claim that no runtime request is needed.
A follow-up will update the client to skip the runtime request using
this information. This commit only encodes the signal into the response.
This moves the dev-mode Cache Components validation renders off the dev
server's main thread onto a worker thread, so rapid navigation no longer
starves the event loop. The worker crosses into the app-page bundle
exactly once, calling a new
`ComponentMod.routeModule.runValidationInDev` entry that rebuilds the
render context, work store, and request store from a serializable
snapshot and runs the whole validation there, so the client prerender
and the user's client components resolve the single app-page React
instance rather than a second copy.
A thin worker shell (`dev-validation-worker.ts`) plus a single-worker
pool (`dev-validation-worker-pool.ts`) load the user bundle via
`loadComponents`, install code-frame support for CLI output, and forward
the validation errors back as Flight bytes; the main thread only
delivers them to the overlay through `sendErrorsToBrowser`. The
snapshot, the globalThis-symbol handoff, and the shared error-delivery
helpers live in their own modules. The dev server installs the worker
when `experimental.devValidationWorker` is not `false`, and
`runDevValidationInBackground` uses it when present, falling back to the
in-process path otherwise. A one-slot `SharedArrayBuffer` propagates a
supersede abort into the worker so a newer navigation cancels an
in-flight validation.
The runtime bundle gains an `app-worker` entry for the worker with the
`build/swc` boundary externalized for the code-frame native binding, and
`patch-error-inspect.ts` now backs its code-frame renderer with a
globalThis symbol so all copies of the module share it across the
thread.
The synthetic `bench/dev-validation` benchmark navigates back-to-back,
so every click lands inside the validation window — the worst case for
main-thread contention. Browser-observed navigation TTFB in that window,
worker vs in-process:
| Route | Worker (p50/p95/max) | In-process (p50/p95/max) |
| ------ | -------------------- | ------------------------ |
| client | 19 / 24 / 27 ms | 40 / 66 / 7762 ms |
| server | 42 / 45 / 46 ms | 122 / 158 / 252 ms |
| sprite | 109 / 117 / 169 ms | 196 / 208 / 299 ms |
The steady-state difference is only tens of milliseconds; the effect
that matters is the tail. In-process, a navigation that collides with an
in-flight validation render can stall for seconds (this run peaked at
~7.8s on the client route, and the peak varies run to run) because a
staged render does not yield until it finishes; off-thread the main
thread stays free and that stall disappears.
Read these as an upper bound, not a speedup that generalizes. The work
moved off-thread is the validation render's CPU — bounded,
route-dependent, and free of IO — so it does not grow with the main
render's cost. In an app whose main render is dominated by IO the same
absolute saving is a small fraction of the request, and it only appears
when a navigation lands in the brief validation window; at ordinary
click speed it is largely invisible. This is a dev-only responsiveness
improvement whose benefit varies widely with the app and the navigation
pattern.
As a follow-up, the validation work that still runs on the main thread
could move to the worker as well. When the main render can't be reused
for validation (for example after a cache miss), the validation
re-renders on the main thread to produce its inputs, resuming from the
Resume Data Cache (RDC) the main render already filled, and only then
hands the resulting Flight chunks to the worker. A later iteration could
run those renders inside the worker too, transporting the (serializable)
RDC so the worker can resume from the filled caches rather than reading
them back on the main thread.
closes NAR-895
This is a behavior-preserving refactor of the dev-mode Cache Components
validation, ahead of the change that moves it onto a worker thread.
In-process behavior is unchanged.
It narrows the context the validation functions require.
`runValidationInDev` and its helpers (`validateStaticShell`,
`warmupClientModulesForStagedValidation`, `validateStagedShell`,
`validateInstantConfigs`) now take a `ValidationRenderContext` (a `Pick`
of `AppRenderContext` plus the two `renderOpts` fields and the
debug-channel flag they actually read) instead of the full render
context, with `toValidationRenderContext` mapping the in-process and
build-time callers. The worker cannot reconstruct a full
`AppRenderContext` from a serialized snapshot, so narrowing the contract
to what validation genuinely consumes is what lets the same code run
there.
It also separates computing the validation errors from delivering them.
`runValidationInDev` now returns the errors instead of sending them to
the dev overlay inline, and the caller `runDevValidationInBackground`
delivers them via `logMessagesAndSendErrorsToBrowser`. The test-mode
lifecycle markers and delay move into `runWithDevValidationLogging`,
which brackets both the render and the delivery. When validation runs on
the worker, the worker computes the errors but the main thread still
delivers them (delivery needs the response object), so splitting the two
halves is a prerequisite.
Finally, it extracts the test-only validation lifecycle markers into a
shared `dev-validation-events` module. The `<VALIDATION_MESSAGE>`
wrapping and the event shape had been hand-rolled at each emission site
in `app-render.tsx` and duplicated again in the test harness; they now
live in one place as `formatValidationEvent` and an exported
`ValidationEvent` union that the in-process emitters call and the test
utilities import rather than redeclare. The worker emits the same
markers through the same helper once it is added, so a single definition
keeps every producer and the test parser in sync. Build validation's
marker `requestId`, previously the numeric `Date.now()`, is stringified
to fit the shared `string` type; it appears only in the logged marker
payload, which nothing matches on, so this too is unobservable.