Commit Graph

3960 Commits

Author SHA1 Message Date
Janka Uryga db66a5094d Create next.config.ts when generating apps with pnpm new-test (#98364)
Updates the template used by `pnpm new-test` for app-dir tests to create
a `next.config.ts` instead of `next.config.js`.
2026-09-08 18:38:36 +00:00
Sebastian "Sebbie" Silbermann 662b0355eb [test] Resolve workspace:* packages to the version under test (#98330)
Test fixtures that depend on monorepo packages (for example
`@next/third-parties` and `@next/mdx`) declared them at the `canary`
dist-tag, which installs the published npm canary instead of the build
under test. In deploy tests this also broke the remote install entirely:
the published canary's `peerDependencies` ranges (for example
`^16.0.0-beta.0`) reject the prerelease preview versions that
`NEXT_TEST_VERSION` installs for `next` (for example
`16.4.0-preview-<sha>-<date>`), so `npm install` failed with ERESOLVE.

Test dependencies can now be declared as `workspace:*`, which resolves
to the build from the current checkout in every test mode:

- dev/start: the locally packed tarball from `pack-for-isolated-tests`,
with a descriptive error when the package has no pack task or is not
part of the repository.
- deploy: the preview tarball of the tested commit when
`NEXT_TEST_VERSION` points at a preview build (preview tarballs already
rewrite their monorepo peer dependencies to the same preview URLs, so
peer resolution succeeds), falling back to the version in the worktree
otherwise. Private packages without published preview tarballs fail with
a descriptive error.

All existing tests that used `canary` for monorepo packages are migrated
to `workspace:*`.
2026-09-08 18:17:29 +02:00
Janka Uryga b0b7a429b9 [PPF] Fix missing runtime-follow up if hints are stale (#98278)
if a page didn't use runtime data during build but started doing so
after the revalidation (i.e. the hints say its statically-prefetchable
but now it's not), we're supposed to be doing a follow-up runtime
request based on the `needsRuntimeRequest` promise (introduced in
#96095), but this was not actually happening.

the bug goes as follows:
1. if we get an entry from a static prefetch, but `needsRuntimeRequest`
is true, we save it as `PPR` (or `StaticShell` if it's a shell rewound
from the static response). otherwise, we'd have saved it as `PPRRuntime`
(or `RuntimeShell` for shells) to indicate that it satisfies those
requests.
2. `wouldRuntimeRequestProvideMore` compares the runtime strategy for
the current static walk to the entry's strategy to check if it should do
a runtime request. this boils down to a comparison of `RuntimeShell >
PPR` for a shell request
3. however, in the `FetchStrategy` enum, `RuntimeShell = 2` and `PPR =
3`, so `RuntimeShell > PPR` is false, which makes
`wouldRuntimeRequestProvideMore` return `false`. so, if we have a `PPR`
entry, **we'll never do a follow-up runtime shell request**.

This is fixed by swapping the order of the two values in the enum.

Now, if we end up with a `PPR` response where `needsRuntimeRequest` is
true due to runtime data, that indicates that we *should've done a
runtime request in the first place*, we just didn't know we should do so
because the prefetch hint was stale, so the new ordering achieves the
correct behavior, i.e. `pingSegmentBundle` decides that it needs a
runtime shell request, which makes it match the intended PPF prefetching
behavior.

The enum ordering is a bit of a lie, because there is no single correct
ordering -- a `PPR` entry *might actually provide more content* than a
`ShellRuntime` entry if the route uses static params. In practice this
is resolved by the vary path (shell responses are keyed with fallback
params, and a PPR entry with static params would have the actual param
values, and the param-ful entry would end up being preferred) so the
strategy order wouldn't matter.

Now that `RuntimeShell` beats `PPR`, i had to disable the strategy
upgrading (`StaticShell -> RuntimeShell` and `PPR -> PPRRuntime`) on
routes that don't use Partial Prefetching -- otherwise, a
rewound`ShellRuntime` entry from a different param was winning over
`PPR` in `pingSegmentBundle`, so it was preventing issuing new static
requests even if we needed to do them. However, without PPF, the runtime
tiers are not meaningful anyway, so this should be fine.

### Other bugs

This fix also revealed a bug where `FetchStrategy.Full` requests were
writing a shell as `RuntimeShell` even though the byte offset only marks
a *static* shell. I've refactored the logic for picking the strategies
to fix this and make it more explicit.

### Edge cases

there is one (somewhat involved) edge case to this that i know of.
if the post-revalidation static response has:

1. a shell that didn't access runtime data (i.e. `needsRuntimeRequest =
false` in the shell stage)
2. a prefetch that did access runtime data (i.e. `needsRuntimeRequest =
true` in the static stage)

then we'll:

1. write the shell as `ShellRuntime` (because `needsRuntimeRequest =
false`, and a runtime shell would not in fact provide more data)
2. write the prefetch as `PPR` (and not `PPRRuntime`, because
`needsRuntimeRequest = true`, and a runtime prefetch *would* provide
more data)

with the new ordering, this means we'll end up preferring the
`ShellRuntime` entry over the `PPR` entry even though the shell is a
strict subset of it and would be missing static params and content gated
behind `prefetch()` or `navigation()`. (note that this only happens if
they share the same vary path)

this can be observed as follows:
1. when a `<Link prefetch={true}>` to such a page is revealed, we won't
request a runtime shell (due to already having a `ShellRuntime` entry),
but we will request a runtime prefetch (because `PPR` is not enough). so
far this is correct
2. if the navigation happens **before** this runtime prefetch completes,
we will display what we already have. but we'll prefer the rewound
`ShellRuntime` shell entry over than the `PPR` entry it originated from
because of the enum ordering. so we may end up not showing content
despite having it in the cache

i'm not sure how to resolve this yet, but i think the current
`recordedFetchStrategy` trick can't handle this, and we need a more
involved mechanism that can detect when the `PPR` entry is actually
better. i don't consider this a blocker for this fix though
2026-09-08 16:22:18 +02:00
Janka Uryga f4449e8fa5 Instant validation for caches excluded from prerenders (#98342)
Follow-up to #98339. Turns out we were not testing how caches excluded
from static/runtime prerenders and app shells behave in Instant
Validation, and it was somewhat broken. This PR adds test coverage and
fixes some bugs I found along the way.

### Fixes

- `use-cache-wrapper` was incorrectly gating cache delays in "request"
stores on `NODE_ENV === "development"`, which does not include
build-time instant validation. The correct check is now implemented in
`isValidationRender`
- `use-cache-wrapper` has divergent behavior for caches with `stale <
MIN_SHELL_STALE` across app shells and PPR/static shells, which needs to
be tracked so that we know that the same render can't be used for both
Instant and Static Shell validation. When we see a cache entry like
that, we now call `trackIncompatibleShellContent()`
- A render that had a cache miss could still report that it's compatible
with both SSV and IV, because we only do the above for a cache hit. As a
result so we'd incorrectly reuse `LAZY_FULL_RENDER` for both. Cache
misses now result in a `trackIncompatibleShellContent()` call to avoid
this

### Tests

We now have tests for:
- `stale < MIN_SHELL_STALE` - excluded from app shells, but included in
static and runtime prefetches
- `stale < MIN_PREFETCHABLE_STALE` - excluded from all prerenders
- `expire < MIN_PRERENDERABLE_EXPIRE` - excluded from static prerenders,
but allowed in runtime prerenders

Due to bugs mentioned above, some of the added tests were failing before
the fixes (mostly the ones that expect an error -- passing a "no
validation errors" test is easy, just don't create any dynamic holes)

In build, these tests incorrectly reported no errors when they should've
failed IV:

- `invalid - unguarded non-prefetchable cache (with short stale)` (both
PPF and non-PPF)
- `non-app shell validation > invalid - unguarded non-prerenderable
cache with short expire` in build:
- `app shell validation > invalid - unguarded cache with a
shorter-than-shell staleTime` in build:

This is because we were missing cache delays in build-time instant
validation (now fixed with `isValidationRender`), so the caches weren't
dynamic holes at all.

The `stale < MIN_SHELL_STALE` tests (`app shell validation > invalid -
unguarded cache with a shorter-than-shell staleTime`) were also failing
in dev:
- **{initial load, client navigation} with cold caches**: Should be an
IV error, but is an SSV error. The initial render had cache misses, so
we did a warm-cache full rerender with runtime shells, which resolved
`await nonShellCache()` in the Runtime stage. But the initial render
**did not track incompatible data**, so we incorrectly re-used it for
SSV and IV. The cache was resolved in `Runtime` so SSV saw a runtime
hole and `await nonShellCache()` errored in SSV with `Next.js
encountered runtime data during prerendering.`
- **client navigation with warm caches**: same as above, except there
weren't cache misses, so incorrectly reused the *original* runtime-shell
render for SSV and IV
- **initial load with warm caches**: No redbox when IV should've
errored. The main render was an initial load and did not use runtime
shells, so it resolved `await nonShellCache()` in `PrefetchStatic`. We
had no cache misses and **did not track incompatible data**, so we
incorrectly re-used the main non-runtime-shell render for SSV and IV.
The cache resolved in `PrefetchStatic` and it wasn't a hole in
`ShellRuntime` so no error was reported.
2026-09-08 16:22:18 +02:00
Janka Uryga e974969ddd [PPF] Model caches with stale < MIN_SHELL_STALE as prefetch-only data (#98339)
Caches that have `stale < MIN_SHELL_STALE` were using
`makeStageHangingPromise` in prerenders, which (in theory) was meant to
track a runtime data access during a static prerender, which in turn
would make the page use runtime shells. This is semantically incorrect,
because both static and runtime shells would exclude the cache, and both
static and runtime prefetches would include it, so it does not actually
signal runtimeness, only prefetchness. The only thing we should be doing
for these caches is excluding the content, same as
`unstable_prefetch()`.

This PR replaces `makeStageHangingPromise` with
`makePrefetchHangingPromise`, which is not tracked as runtime data at
all. We also use it for `unstable_prefetch()` to signal the
correspondence.

Note that in practice, the usage of `makeStageHangingPromise` in
`use-cache-wrapper` was *technically* fine, because its runtime data
tracking was effectively dead code. We only returned it if
`stagedRendering.finalStage < stage`, but for static prerenders `stage =
staticLinkDataStage = PrefetchStatic` and `finalStage = Static`, so the
condition was never actually true, and we never actually tracked it as a
runtime data access. However semantically it should still not be tracked
as runtime data.
(The condition *can* be true in a runtime prerender that only prerenders
the shell. We don't do any tracking in runtime prerenders, so it was
also a no-op).

(this was extracted from #98129, where we separate runtime data access
tracking into session data and URL data. i couldn't figure which one
`makeStageHangingPromise` should be tracked as, but then i realized it
shouldn't be tracked at all)
2026-09-08 16:22:17 +02:00
Jimmy Miller 9d9e1d5b00 Fix turbopacks webpack loader setup to support options being optional (#98270)
If you look at the types for webpacks loaders, you can see that options
is optional.
https://github.com/webpack/webpack/blob/c17742f631eaeaf1ad880a70b105eedd96dd21b9/declarations/LoaderContext.d.ts#L83

The implementation between the two is slightly different so webpacks
impementation doens't jsut set it to {}. But with the way turbopack
works, this is the equivalent.

I saw a bunch of failures all related to this in real loaders, so figure
it was an easy quick fix.

(also extensionOptions is optional in webpack and needed to be here for
the the types to all checkout
https://github.com/webpack/webpack/blob/c17742f631eaeaf1ad880a70b105eedd96dd21b9/declarations/WebpackOptions.d.ts#L2175)
2026-09-08 06:38:33 -07:00
Tobias Koppers 7167297192 test: stabilize server action HMR update (#98322)
### What?

Stabilize the development-mode server action HMR test when an unrelated
Fast Refresh reload occurs while the patched action is being applied.

### Why?

The test retried by dispatching another click before the previous
asynchronous server action had settled. Under CI load, those requests
overlapped with HMR and an unrelated full-page Fast Refresh reload. The
reload reset the client counter, so the correctly updated action
returned exactly `1000`, which the test rejected because it assumed the
pre-reload state was preserved.

### How?

Wait for network idle after each action invocation so retries do not
observe an in-flight response, allow the valid post-reload value of
`1000`, and use a retry window that accommodates CI HMR latency. The
assertion still distinguishes the updated `+1000` action from the
original `+1` implementation without requiring client state to survive
an unrelated reload.

### Verification

- `pnpm test-dev-turbo
test/e2e/app-dir/actions/app-action-node-middleware.test.ts -t "should
support updating the action"` (10 consecutive passes)
- `pnpm test-dev-webpack
test/e2e/app-dir/actions/app-action-node-middleware.test.ts -t "should
support updating the action"` (4 consecutive passes)
- Prettier and ESLint on `test/e2e/app-dir/actions/app-action.test.ts`

<!-- NEXT_JS_LLM -->


<!-- fleet 5793f439-a569-44be-ad19-d456ce9b641c -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-08 10:40:35 +02:00
Janka Uryga 00b29d6654 fix: track dynamic accesses in final runtime prerenders (#98255)
The fix implemented in #95665 (`params/searchParams` not hanging
correctly in Runtime App Shells during prospective prerenders) was
incomplete. We started using `makeHanging[Search]Params` in the
prospective prerender (if it was an app shell and wouldn't resolve
them), but not during the final prerender. This meant that, when
encoding the cache key, we would only fire `dynamicAccessAbortSignal` in
the prospective prerender:


https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2160-L2174

...but the final prerender wasn't using `makeHanging[Search]Params`, so
it would not hit this early exit. The hanging input abort signal would
still correctly abort the key encoding (because it accounts for
`stageController.finalStage`) so the key would still be partial, as
expected. We would continue on, and attempt to look the key up in the
Resume Data Cache. But the prospective prerender exited early and never
put anything under that key, so we would incorrectly end up here, and
log:


https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2730-L2734

This is fixed by consistently using `makeHanging[Search]Params` if the
runtime prerender won't reach the stage where they resolve, i.e. if it's
an App Shell. This means we'll trigger `dynamicAccessAbortSignal` while
encoding the cache key in *both* renders, so we'll do the early exit
consistently and avoid the log.

(This feels a bit unsatisfying, because ideally this would all just work
based on stages, which is what I idealistically thought when writing the
code originally. But unfortunately our usage of
`dynamicAccessAbortSignal` for `params/searchParams` requires these
special cases to work consistently. Maybe this can be improved in the
future.)

### Tests

This scenario is covered by the existing `prefetch-runtime` tests -- i
discovered the bug by running them and seeing the `Unexpected cache miss
after cache warming phase` warning. We now assert that the warning is
not logged. The assertion is negative and would also pass if the error
message changed, so i've exported the message as a constant to ensure
`use-cache-wrapper` and the tests stay in sync.
2026-09-04 22:34:16 +02:00
Marcos Hernanz 090f1b7275 fix: suppress missing package manager errors in next info (#97946)
## What

- capture stdout and stderr for both direct and shell-fallback
package-manager version probes
- keep successful version output unchanged and return `N/A` when a probe
fails
- add deterministic cross-platform regression coverage with a failing
Yarn shim

Closes #97932

## Why

PR #71134 added an `execSync` fallback so package-manager command shims
can be resolved on Windows. When `stdio` is omitted, Node mirrors stderr
from failed synchronous child processes to the parent, so an unavailable
Yarn or pnpm can print raw shell errors before `next info` reports
`N/A`.

Passing `{ stdio: "pipe" }` to both probes preserves captured stdout and
the Windows fallback while keeping expected probe failures silent. The
change stays scoped to package-manager version detection; shared
registry discovery is unchanged.

## Testing

- `pnpm --filter next typescript`
- `pnpm --filter next build`
- `pnpm test-types --pretty false`
- ESLint and Prettier on both changed files
- `pnpm test-start test/e2e/cli/cli.test.ts` (66 passed, 1 skipped)
- built-CLI reproduction with npm available and Yarn/pnpm absent: both
report `N/A` with no Yarn/pnpm command-not-found output
- autoreview with GPT-5.6 Sol xhigh and Claude Opus 5 xhigh: zero
findings

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-09-03 18:12:15 -07:00
Josh Story 65c832c8b8 Return client errors for unrecognized Server Actions (#98123)
## Summary

- return `400 Bad Request` when a Server Action reference ID does not
match the expected format
- return `409 Conflict` when a well-formed action ID is unavailable in
the current deployment, including deployment skew
- apply the same classification to IDs from the `Next-Action` header and
multipart MPA forms, including direct and bound actions in Node and Edge
runtimes
- return generic text bodies matching the client error category, without
exposing action-ID details
- derive marked forwarded-action responses from the original action ID
so an unexpected or mixed-version worker response cannot change the
client error semantics

Fetch actions previously had dedicated 404 handling, while equivalent
MPA form submissions fell through to the generic error path and returned
500. This removes that transport-dependent behavior and distinguishes
malformed or scanning traffic from legitimate stale clients without
treating either case as a missing page or an internal server failure.

## Verification

- `pnpm test-dev-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo test/e2e/app-dir/actions/app-action.test.ts -t
'forward.*action'`
- `pnpm test-start-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/no-server-actions/no-server-actions.test.ts`

<!-- NEXT_JS_LLM -->
2026-09-03 10:36:53 -07:00
Tim Neutkens fb4c498852 Upgrade web-vitals to v6 and report soft navigations (#97232)
## Summary

Upgrades the vendored `web-vitals` dependency to v6.0.1 and enables
soft-navigation reporting for CLS, INP, and LCP. This lets
`useReportWebVitals` report metrics against the navigation where they
occurred, including `navigationURL` for route attribution.

The upgrade also removes the obsolete FID integration and compatibility
patch, regenerates the standard and attribution bundles, and updates the
public metric types and documentation to match the current upstream API.

Related to #97161.

## Verification

- Added production-mode coverage for initial and soft-navigation Web
Vitals reporting with both Turbopack and Webpack.

<!-- NEXT_JS_LLM -->
2026-09-03 09:45:19 +02:00
Sebastian "Sebbie" Silbermann 5b8c866c32 [test] Skip hooks of suites skipped by a lazy @force-gate (#98169)
A `describe` gated by a lazy `// @force-gate` (a condition read off the
fixture's resolved config, like `cacheComponents`) cannot be skipped at
collection time, so the gate runtime force-passes its tests at runtime
and `nextTestSetup` skips the fixture build. Hooks registered inside
such a `describe` still ran, because only the `it`/`test` globals were
wrapped. A hook that touches the `next` instance (for example a
`beforeEach` that reads `next.cliOutput`) then failed with `next
instance is not initialized yet` and failed the supposedly skipped
suite.

The gate runtime now wraps the
`beforeAll`/`afterAll`/`beforeEach`/`afterEach` globals as well and
skips a hook registered under a lazy `@force-gate` when that gate
evaluates false against the fixture's resolved config. Only a false lazy
force-gate skips a hook: an inverted `@gate` still runs its tests and
therefore needs its hooks, and a static `@force-gate` already skips the
whole `describe` at collection time. The hooks `nextTestSetup` registers
itself are marked with the new `ungatedHook` helper so that the skip
decision and the fixture cleanup still run.

The `cacheComponents disabled, edge app router` describe in
`test/e2e/cache-handlers-upstream-wiring` is converted from the
`process.env.__NEXT_CACHE_COMPONENTS` `describe.skip` ternary to `//
@force-gate !cacheComponents`, replacing a fake-green skip and
exercising the fixed path. The `!deploy` conversion of the remaining
describes is left to the branch that introduces the `deploy` condition.

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-09-02 19:45:27 +02:00
Niklas Mischkulnig 538f3f68e9 Bump swc to v77 (#97407)
There have been breaking changes to the AST:
- `BlockStmtOrExpr` is now `ArrowFunctionBody`
- function bodies are `FunctionBody` instead of `BlockStmt`
- JSX text literals are `Wtf8Atom`, instead of `Atom`
2026-09-02 16:09:18 +02:00
Luke Sandberg bcfa15d0ed Remove the worker thread for turbopack builds (#96862)
Avoid spawning a worker thread to run turbopack

The typical arguments for worker threads are that they isolate the JS
heap and enable parallelism, but turbopack does both these things by
default (native heap is isolated, the js deps are minor and have a lot
of overlap with existing next server deps).

There is also no substantial parallelism opportunity from worker threads
that aren't immediately delivered by turbopacks execution model

## Benchmarks

Measured with a paired A/B harness that alternates `head` and `base`
builds so thermal drift and background load cancel out, discarding a
warmup build per variant and clearing `.next` before each run. Both
variants are the same commit except for this change. 35 paired
iterations on a default `create-next-app` (App Router, TS), Node
20.11.1, Apple silicon.

| Metric | this PR | canary | Diff | 95% CI | Wins |
| --- | --- | --- | --- | --- | --- |
| `next-build` total | 2478 ms | 2595 ms | **-117 ms (4.5%)** | [50,
183] | 30/35 |
| `run-turbopack` | 1228 ms | 1335 ms | -107 ms (8.0%) | [62, 152] |
31/35 |
| overhead (total - turbopack) | 1250 ms | 1260 ms | -10 ms (0.8%) |
[-27, 46] | 20/35 |
| wall clock | 2775 ms | 2888 ms | -112 ms (3.9%) | [45, 180] | 30/35 |
| peak RSS | 1175 MiB | 1195 MiB | **-19 MiB (1.6%)** | [3.4, 35.3] |
22/35 |

Significant by both a paired t-test and a Wilcoxon signed-rank test (p =
0.0001 for total build time, p = 0.025 for RSS). The Wilcoxon is
included because a couple of slow builds per run would otherwise
dominate the variance.

Nearly all of the saving lands inside the `run-turbopack` span rather
than around it, which is expected: that span wraps the worker spawn, the
IPC to marshal the build across, and the teardown, so deleting the
worker removes work from inside it.

The win is proportionally smaller on larger apps, where compilation
dominates. On `bench/basic-app` (~400 source modules, ~19s builds) the
same ~115 ms is well inside the noise -- 12 paired iterations there
showed no significant difference on any metric. So this is a real but
small improvement that is only user-visible on small builds:

| Metric | this PR | canary | Diff | 95% CI |
| --- | --- | --- | --- | --- |
| `next-build` total | 19313 ms | 19436 ms | -123 ms (0.6%) | [-256,
502] |
| overhead (total - turbopack) | 1424 ms | 1415 ms | +9 ms (0.6% slower)
| [-52, 70] |

Caveats: single machine and two fixture shapes. Peak RSS is the build
process's own footprint (`/usr/bin/time -l`), which includes the worker
thread's isolate but not the separately spawned static-generation
workers. A CI runner with fewer cores would plausibly show a larger
effect.
2026-09-01 22:27:43 -07:00
Aurora Scharff e9180eae2a errors: shorten "use cache" messages and unify them into one factory (#94300)
### What?

Centralizes related <code>"use cache"</code> scope errors and rewrites
them as shorter, actionable messages with consistent <code>Learn
more:</code> links. It also updates the reachable revalidation errors
for rendering and <code>generateStaticParams</code>.

### Why?

The previous messages were long, inconsistent, and sometimes missing
documentation links. Some new cache-specific revalidation messages were
also hidden by an earlier render-phase error. The new messages name the
constraint, give the immediate fix, and link to the relevant
documentation.

### How?

Adds centralized error factories and dedicated error pages for request
data, cache configuration, private-cache composition, and revalidation.
When an active App Router route is available, the messages include it.
The error pages use consistent terminology and complete examples. The
<code>unstable_cache()</code> reference now documents the
request-dependent operations that these errors reject.

### Before and after

Route-based examples use <code>/products</code>. Bracketed values are
alternatives in matching order. For example, <code>[E1482, E1486,
E1489]</code> maps to <code>[headers(), cookies(), request.url]</code>.
Each runtime error contains one alternative, but the table groups
messages with the same template.

#### Messages that include the route

| Cases | Before | After |
| --- | --- | --- |
| E1480 · <code>searchParams</code> in <code>"use cache"</code> | Route
/products used <code>searchParams</code> inside "use cache". Accessing
dynamic request data inside a cache scope is not supported. If you need
some search params inside a cached function await
<code>searchParams</code> outside of the cached function and pass only
the required search params as arguments to the cached function. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": <code>searchParams</code> can't be read inside
<code>"use cache"</code>. Await it outside the cached function and pass
what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1482, E1486, E1489] · [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] in <code>"use
cache"</code> | Route /products used [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] inside "use cache".
Accessing Dynamic data sources inside a cache scope is not supported. If
you need this data inside a cached function use [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] outside of the cached
function and pass the required dynamic data in as an argument. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": [<code>headers()</code>, <code>cookies()</code>,
<code>request.url</code>] can't be read inside <code>"use cache"</code>.
Read it outside the cached function and pass what you need as an
argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1481, E1485, E1492] · [<code>cookies()</code>,
<code>request.url</code>, <code>headers()</code>] in
<code>unstable_cache()</code> | Route /products used
[<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] inside a function cached with
<code>unstable_cache()</code>. Accessing Dynamic data sources inside a
cache scope is not supported. If you need this data inside a cached
function use [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] outside of the cached function and pass the
required dynamic data in as an argument. See more info here:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
Route "/products": [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] can't be read inside
<code>unstable_cache()</code>. Read it outside the cached function and
pass what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
| [E1484, E1491] · <code>draftMode().enable()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used "draftMode().enable()" inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The enabled status of
<code>draftMode()</code> can be read in caches but you must not enable
or disable <code>draftMode()</code> inside a cache. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>draftMode().enable()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>]. Draft mode
can be read inside a cached function, but enabling or disabling it must
happen outside.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| [E1488, E1499] · <code>connection()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used <code>connection()</code> inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The
<code>connection()</code> function is used to indicate the subsequent
code must only run when there is an actual request, but caches must be
able to be produced before a request, so this function is not allowed in
this scope. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>connection()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>] because cached
functions may run during prerendering, without an incoming request. Call
it outside the cached function.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| E1494 · <code>connection()</code> in <code>"use cache: private"</code>
| Route /products used <code>connection()</code> inside "use cache:
private". The <code>connection()</code> function is used to indicate the
subsequent code must only run when there is an actual navigation
request, but caches must be able to be produced before a navigation
request, so this function is not allowed in this scope. See more info
here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route
"/products": <code>connection()</code> can't be called inside <code>"use
cache: private"</code> because private cached functions may run during
prefetching, without a navigation request. Call it outside the cached
function.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/directives/use-cache-private |
| [E1483, E1495] · <code>revalidateTag("products")</code> in [<code>"use
cache"</code>, <code>unstable_cache()</code>] | Route /products used
"revalidateTag products" inside [a "use cache", a function cached with
"unstable_cache(...)"] which is unsupported. To ensure revalidation is
performed consistently it must always happen outside of renders and
cached functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| During render | Route /products used "revalidateTag products" during
render which is unsupported. To ensure revalidation is performed
consistently it must always happen outside of renders and cached
functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| Inside <code>generateStaticParams</code> | Route /products used
"revalidateTag products" inside <code>generateStaticParams</code> which
is unsupported. To ensure revalidation is performed consistently it must
always happen outside of renders and cached functions. See more info
here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called inside <code>generateStaticParams</code>. Call it from a Server
Action or Route Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |

#### Other messages

| Cases | Before | After |
| --- | --- | --- |
| [E1477, E1490] · Nested cache with [short <code>expire</code>,
<code>revalidate: 0</code>] | A "use cache" with [short
<code>expire</code> (under 5 minutes), zero <code>revalidate</code>] is
nested inside another "use cache" that has no explicit
<code>cacheLife</code>, which is not allowed during prerendering. Add
<code>cacheLife()</code> to the outer "use cache" to choose whether it
should be prerendered [with longer <code>expire</code>, with non-zero
<code>revalidate</code>] or remain dynamic [with short
<code>expire</code>, with zero <code>revalidate</code>]. Read more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
| Route <code>"/products"</code>: A nested <code>"use cache"</code> with
[a short <code>expire</code> (under 5 minutes), <code>revalidate:
0</code>] is inside an outer <code>"use cache"</code> that has no
<code>cacheLife()</code>. Add <code>cacheLife()</code> to the outer one
to choose whether to prerender it [with a longer <code>expire</code>,
with a non-zero <code>revalidate</code>] or keep it dynamic [with a
short <code>expire</code>, with <code>revalidate:
0</code>].<br><br>Learn more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
|
| E1478 · External promise | Filling a "use cache" entry appears to be
stuck on shared state from the outer render scope. The same function
completed when run in isolation, which usually means a module-scoped
value (for example a top-level Map used to dedupe fetches) is joining a
promise created outside the cache. "use cache" already dedupes calls
with the same arguments within a request and across requests on the same
server instance, so the surrounding dedupe layer is both unnecessary and
the likely cause. Remove it and rely on "use cache" alone for
deduping.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function is
awaiting a promise created outside it. The same call completed when run
in isolation, so a module-scoped value (often a top-level
<code>Map</code> used to dedupe fetches) is most likely blocking it.
<code>"use cache"</code> already dedupes calls with the same arguments.
Remove the surrounding dedupe layer.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1479, E1498] · [<code>cacheTag()</code>, <code>cacheLife()</code>]
outside a cached function | [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a "use cache"
function.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a <code>"use
cache"</code> or <code>"use cache: private"</code>
function.<br><br>Learn more:
[https://nextjs.org/docs/messages/cache-tag-outside-use-cache,
https://nextjs.org/docs/messages/cache-life-outside-use-cache]<br><br>Outside
an App Router route, the same message is shown without the route prefix.
|
| E1487 · Prerender timeout | Filling a cache during prerender timed
out, likely because request-specific arguments such as params,
searchParams, cookies() or dynamic data were used inside "use
cache".<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function took too
long during prerendering. The most common cause is passing unresolved
request-specific arguments, such as <code>params</code> or
<code>searchParams</code>, into the cached function. Resolve the data
before calling the function and pass only the values you
need.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| E1493 · Private cache inside a public cache | "use cache: private"
must not be used within "use cache". It can only be nested inside of
another "use cache: private".<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be nested inside <code>"use cache"</code> because a shared cached
function can't depend on private request data. Nest it only inside
another <code>"use cache: private"</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1496 · Private cache without a request | "use cache: private" cannot
be used outside of a request context.<br><br><em>No documentation
link.</em> | Route <code>"/products"</code>: <code>"use cache:
private"</code> needs an active request, so it can't be used during
<code>generateStaticParams</code> or other build-time contexts. Move it
to a request-time component or function.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1497 · Private cache inside <code>unstable_cache()</code> | "use
cache: private" must not be used within
<code>unstable_cache()</code>.<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be used inside <code>unstable_cache()</code> because
<code>unstable_cache()</code> uses a shared cache that can't contain
private request data. Call the private cached function outside
<code>unstable_cache()</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |

### Runtime verification

- Exercised the 13 route-based message IDs through minimal dev-runtime
reproductions. Twelve reached the new factories from userland, with
byte-identical output across three requests each.
- The `request.url` error for `"use cache"` Route Handlers is currently
limited to the prerender path. A dynamic request captured by a cached
closure can bypass that tracking; this is existing framework behavior to
follow up separately.
- Passing the `searchParams` promise into a nested cached function can
still surface the earlier synchronous dynamic-API error before this
factory. The new message is verified when the cache scope reads its own
`searchParams` value.
- A follow-up preview verification triggered 17 of the 19 rewritten
messages from userland with byte-identical output. The timeout-driven
external-promise and prerender-timeout messages were verified in source
and through their focused test coverage.

### Verification

- <code>CI=1 pnpm build-all</code>
- <code>pnpm --filter=next types</code>
- <code>pnpm --filter=next build</code>
- <code>HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>NEXT_SKIP_ISOLATE=1 HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/cache-components-errors/use-cache.test.ts -t 'cacheLife
with (expire &lt; 5 minutes|revalidate: 0)'</code> (6 tests and 6
snapshots passed)
- <code>pnpm test-dev
test/e2e/app-dir/cache-components-errors/use-cache.test.ts --projects
jest.config.*</code> (42 tests and 42 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts --projects
jest.config.*</code> (10 tests and 8 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-configured-timeout/use-cache-configured-timeout.test.ts
--projects jest.config.*</code> (4 tests and 2 snapshots passed across
Turbopack and webpack)
- Prettier, ESLint, and Alex on the changed source, tests, and error
pages

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 00:29:56 +02:00
Janka Uryga adba1e3f2a [PPF] Pass in original searchParams for private-cache pages (#98041)
The handing for page components marked with `use cache: private` was
assuming that searchParams can never hang in `prerender-runtime`, so it
passed the serialized `innerSearchParams` to the page, which we expected
to just be a resolved promise. This *used to* be correct when runtime
prerenders always had search params available, because the serialized
search params were always equivalent to `outerSearchParams`.

However, for `partialPrefetching` we started using `prerender-runtime`
for runtime shells, where search params are not available, and if a
private cache awaits them, it needs to abort filling and become dynamic,
same as when it awaits params. So we need to trigger
`dynamicAccessAbortController` when `searchParams` is awaited:

```tsx
export default async function Page({ searchParams }) {
  'use cache: private'
  // this should trigger `dynamicAccessAbortController.abort()`, so we need the original instrumented
  // searchParams object instead of the serialized one
  await searchParams
}
```

This is fixed by using the outer searchParams object in the
`isPageSegmentFunction` codepath of `use-cache-wrapper` -- we're
preserving the instrumented promise, so the cache prerender will abort
as expected.
(before this fix, a shell prefetch of such a page would get a
deserialized hanging promise for `searchParams` and hang until it times
out)

2 of the 3 newly added tests ("params in a public cache" and "params in
a private cache") were already passing, because `params` was already
being preserved, but i'm adding them here to prevent regressions. The
"search params in a private cache" one was failing due to the cache
timing out, and is now passing. I'm not covering "search params in
public cache" because that's an error and is already tested elsewhere.
2026-08-31 19:15:43 +02:00
Niklas Mischkulnig 8330e4c4cd test: Improve test cache handler implementations (#98098)
- Some of them were not forwarding `getExpiration` or `softTags` to
`defaultCacheHandler`
- use-cache-cross-deployment was ignoring softTags and expiration. (This
patch was written by Sol)

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-08-31 13:59:14 +02:00
Marcos Hernanz a41e98e725 Fix optimistic routing for encoded dynamic params (#97948)
## Summary

Fixes #97842 by canonicalizing encoded pathname parts before using them
as dynamic segment cache keys in optimistic route predictions. This
keeps predicted route trees consistent with server Flight data and
prevents query-only navigation from remounting pages when regular or
catch-all params contain encoded characters.

Add coverage that verifies bfcache IDs and form state are preserved for
`@` in a regular dynamic param and for `@` plus `%2F` in a catch-all
param.

## Verification

- Existing encoded-slash route-cache E2E suite in Turbopack and webpack
production modes
- Existing optimistic-routing E2E suite in Turbopack and webpack
production modes
- `pnpm test-types`
- `pnpm types`
- `pnpm prettier-check`
- `pnpm lint-ast-grep`
- `pnpm lint-language`
- Changed-file Prettier and ESLint checks via the pre-commit hook

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-08-28 15:50:36 -07:00
Marcos Hernanz 4d36801fdb Fix intercepted route params after Proxy rewrites (#97953)
## What?

Preserve the final internal rewrite pathname in the RSC response when an
earlier Proxy rewrite has already populated the rewritten-path header.

This also adds an end-to-end regression assertion for a dynamic
interception route behind a locale-injecting middleware rewrite.

## Why?

The route resolver set the final marker-bearing pathname directly on the
response, but router-server later reapplied the accumulated Proxy
response headers. That restored the earlier markerless pathname. The
client then parsed an intercepted dynamic param against that stale path
and removed three real characters from the value.

## How?

Mirror the final internal rewrite pathname into the accumulated
response-header map. When router-server applies that map after route
resolution, the last rewrite remains authoritative.

Fixes #97939

## Tests

- Production interception regression suite: 4/4 passed
- Development interception regression suite: 4/4 passed
- Related production rewrite/interception suites: 43/43 passed
- Next package build, targeted ESLint, Prettier, and git diff checks
passed
2026-08-28 15:50:12 -07:00
Janka Uryga d8f95c12de [PPF] Fix navigation() in prospective runtime prerenders (#98000)
### Long story short

Fixes an analogous bug to #95665, but affecting
`prefetch()/navigation()`. Those two were accidentally always resolving
in the **prospective** runtime prerender, so we were warming content
that isn't needed for the final runtime prerender. We now guard them
properly.

Also, I replaced `PrerenderStoreModernRuntime.isSessionShell` with
`finalStage` (the same stage we're gonna use for the final prerender),
so we can include/exclude `navigation` properly -- the boolean only
distinguished shell/prefetch, not navigation.

### Long story long

We use runtime prerenders for three things: runtime shells, runtime
prefetches, and embedding a prefetch in navigations. These are aborted
after `ShellRuntime`, `Runtime`, and `NavigationRuntime` respectively
(accessible in `stageController.finalStage`). If we're doing the
**final** prerender for a shell, promises that would resolve in e.g.
`NavigationRuntime` would never resolve, because we abort the prerender
before we get there.

However, the **prospective** runtime prerender has no
`StagedRenderingController` and needs to model this separately (so that
the same APIs hang in both). This was [previously done via
`prerenderStore.isSessionShell`](https://github.com/vercel/next.js/pull/95665)
which we checked in URL data apis i.e. `params/searchParams` and the
metadata`pathname` to do this. `isSessionShell: true` meant they should
hang because it's a shell, and `isSessionShell: false` meant they should
resolve.

However, I forgot about this case when implementing
`navigation`/`prefetch`, and made them resolve unconditionally in the
prospective prerender. This means we'd potentially warm content that
would not actually be reached in the final prerender. In other words,
```ts
await navigation()
return <Expensive />
```
would needlessly render `<Expensive />` during the prospective render
but not in the final one. This kinda defeats `navigation`'s main
purpose, i.e. avoiding expensive work.

This PR fixes the bug by replacing `isSessionShell: boolean` with
`finalStage: AdvanceableRenderStage`, which is set to the same value we
put in `stageController.finalStage`. This lets us keep the logic of what
hangs and what resolves relatively close in both prerenders.

---

On a meta level, the fact that the same bug happened twice tells me that
having a `StageRenderingController` that is sometimes `null` is
error-prone, and we should figure out a better solution, but i'm gonna
keep this fix targeted and figure out a more holistic solution in a
follow-up
2026-08-28 22:10:21 +02:00
Niklas Mischkulnig e3790aa911 More granular cache keys for use-cache entries (#95233)
Caveats:
- I had to skip `react[-dom][/*]` and
`private-next-rsc-server-reference` and `private-next-rsc-cache-wrapper`
imports in the code hash and env-var tracking. Because those all end up
pulling in app-page-turbo.runtime.prod.js which reads many env vars and
would cause constant deopting. But this should still be correct. The
code of these imports is included via the Next.js version, and no env
vars should change the semantics of any of those imports.
- Static env var reads are collected, but too dynamic accesses are
silently ignored and don't lead to deopts (same goes for env var reads
in native NAPI addons). This means that this cache reuse is not
guaranteed to be 100% guaranteed to never lead to stale caches.
- Code hashing and env var collection happens on a per-module basis. So
if you put all use-cache functions into a single file and/or together
with react components, then you will see extraneous invalidations. This
will be fixed by either generally enabling module splitting for all of
Turbopack, or by adding a special transform that does it for use-cache
functions.

Followups:
- There are some env var static analysis gaps that will be immediate
followups before broader testing. These are the various TODOs added in
https://github.com/vercel/next.js/pull/95310
- Client components invalidation is very coarse grained right now
(statically imports any client component anywhere -> deopt completely).
Followup for the future. But the current setup is correct. This would
just improve effectiveness further
- Do this in dev as well. Currently there is no NFT at all in dev (for
performance reasons)
- Module splitting for more granular tracking
- Include entropy when serializing server reference arguments for cache
key


Todo:

- [x] Use implementation code hash (includes inlined env vars): from
#94234
- [x] Include non-inlined runtime env vars: from #95310
- [x] Include client reference manifest (very coarse for now)
- [x] Include Next.js version (for wire format, etc)
- [x] This is now done for all use-cache entries now. Not just for `use
cache: remote`. Is that the intended behavior? Yes
- [ ] ~~if `NEXT_DEPLOYMENT_ID` is in the env vars. just deopt and don't
care about stringifing and hashing the env vars~~
- [x] Is cache key size a problem? Currently you can get this: (values
are always hashed)
`CustomCacheHandler::get
["80e6f6560092f0078775e7e787c1c10ecf6dea0bc4",[],["d984fbaa996274737f3b59345a300a20","16.4.0-canary.5","__NEXT_NO_MIDDLEWARE_URL_NORMALIZE=undefined","NEXT_OTEL_PERFORMANCE_PREFIX=undefined","__NEXT_PRIVATE_ORIGIN=a04f4b9d6a8f42724740a480e9a2bc67c053dc04377be831c0e2c407a1422004","NEXT_PRIVATE_RESPONSE_CACHE_TTL=undefined","NEXT_PRIVATE_RESPONSE_CACHE_MAX_SIZE=undefined","__NEXT_CACHE_COMPONENTS=b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b","__NEXT_ROUTER_BASEPATH=undefined","__NEXT_MANUAL_CLIENT_BASE_PATH=undefined","__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS=undefined","__NEXT_APP_NAV_FAIL_HANDLING=undefined","__NEXT_GESTURE_TRANSITION=undefined","__NEXT_USE_OFFLINE=undefined","NEXT_DEBUG_BUILD=undefined","__NEXT_VERBOSE_LOGGING=undefined...]]
[["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]]`
2026-08-28 10:17:15 -07:00
Tobias Koppers 17a901a74f Guard filesystem reads against unresolved symlinks (#97902)
### What?

Adds debug-only OS realpath validation to successful `DiskFileSystem`
file and directory reads. When a successfully canonicalized path differs
from the supplied path, the read returns a normal task error naming both
paths.

Fixes pattern/glob traversal and NFT tracing so physical filesystem
access uses resolved paths while logical paths remain available for
user-visible specifiers and complete symlink-chain recovery.

### Why?

Reading through an unresolved symlink parent gives the same filesystem
object multiple path identities. That can make Turbo Tasks dependency
tracking and invalidation inconsistent and can produce invalid
deployment ZIPs when NFT output contains files below unresolved links.

The checks return errors rather than asserting because paths can
disagree temporarily under eventual consistency. Propagating a task
error avoids panicking a worker thread while still exposing invalid
callers during development.

### How?

The validation lives directly in `DiskFileSystem::read` and
`DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API
inline instead of the Turbo Tasks realpath task, keeping the diagnostic
out of the task dependency graph. The guard runs only after the OS read
succeeds, so missing/non-directory probes preserve their existing
behavior.

`read_matches` resolves each physical directory immediately before
enumeration while retaining logical `PatternMatch` paths.

`read_glob` and `track_glob` now resolve their initial directory before
enumeration. Symlinks discovered later through wildcard segments are
also traversed through resolved targets. `ReadGlobResult` deliberately
retains logical paths rooted at the supplied base, allowing consumers to
call `realpath_with_links` and recover the complete symlink chain.

Consumers follow that contract explicitly:

- NFT includes expand each logical match with `realpath_with_links`,
emit resolved files and every traversed symlink, skip resolved directory
targets, and deterministically deduplicate/sort output.
- `import.meta.glob` uses recursive logical keys as the source of
user-visible requests, while module resolution follows and tracks
symlinks.
- the hash-glob example resolves returned logical paths before reading.

Webpack-loader context dependencies are covered for both
`path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader
fixture performs its directory read with Node `fs`, reports the
directory using `addContextDependency`, and Turbopack tracks the
resolved target.

### Verification

- `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --
--check`
- `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api
--all-targets`
- `cargo test -p turbo-tasks-fs` (128 passed)
- `cargo test -p next-api` (7 passed)
- `cargo check -p turbo-tasks-fs --examples`
- `import.meta.glob` symlink execution fixture (1 passed)
- Nine targeted node-file-trace CI cases with `release-with-assertions`
(9 passed)
- `pnpm build-all`
- `webpack-loader-fs` Turbopack dev e2e (1 passed)
- `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed)
- twoslash Turbopack production, normal mode (4 passed)
- twoslash Turbopack production, cache-components mode (4 passed)
- `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200)

### Notes

The disk guard is cross-platform, while its symlink-parent regression
test is Unix-only, matching neighbouring symlink tests. On Windows, OS
canonicalization can also normalize casing and 8.3 short names; a debug
read using a non-canonical spelling will therefore return the same
diagnostic error.

<!-- NEXT_JS_LLM -->


<!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-28 08:31:06 +02:00
Josh Story 6d6228c0c6 Prune incomplete parallel route matchers (#97108)
## Summary

This adds an experimental `strictRouteMatching` flag that leaves a
matcher out when its finalized loader tree contains a synthesized
default that will always call `notFound()` for a slot actually declared
by the owning layout. Adding a `default.tsx` keeps the matcher, and
structural router-state branches that are not real slots of that layout
do not make it incomplete. The flag defaults to `false` so matcher
pruning can roll out independently from the preceding loader-tree
correction.

The main goal is to make `children` and named slots behave the same way.
If a URL cannot construct a complete route tree on its own, it should be
treated as an unmatched URL instead of sometimes showing a slot 404 and
sometimes producing a missing page or default error.

## Semantics

For example:

```text
app/split-matcher/
├── layout.tsx
├── foo/page.tsx
├── bar/page.tsx
└── @slot/[...parts]/page.tsx
```

With strict route matching disabled, Next.js emits matchers for
`/split-matcher/foo`, `/split-matcher/bar`, and the broad
`/split-matcher/[...parts]`. The broad matcher can match any URL, but
its `children` branch has neither a matching page nor a default, so it
can only construct a permanent 404 tree. With strict route matching
enabled, that broad matcher is omitted while `/foo` and `/bar` remain
because each combines a real `children` page with the named-slot
catchall. Adding `app/split-matcher/default.tsx` deliberately makes the
broad matcher complete and keeps it.

The same rule applies without a catchall:

```text
app/disagreeing-slots/
├── layout.tsx
├── @first/foo/page.tsx
└── @second/bar/page.tsx
```

`/foo` is incomplete because `@second` has no matching page or default,
and `/bar` is incomplete for the corresponding reason in `@first`, so
neither matcher is emitted. A later PR in this stack reports the
now-unreachable page files as a project misconfiguration.

A declared `children` route is treated like any named slot:

```text
app/declared-children/
├── layout.tsx
├── page.tsx
└── @panel/
    ├── default.tsx
    └── details/page.tsx
```

`/declared-children` is complete because `children` uses `page.tsx` and
`@panel` uses its default. `/declared-children/details` is incomplete
because `@panel` matches its page while the declared `children` slot has
no matching page or default at `/details`, so that matcher is pruned.

A route composed entirely from declared named slots is also complete:

```text
app/named-only/
├── layout.tsx
├── @left/[...slug]/page.tsx
└── @right/[...slug]/page.tsx
```

The preceding PR's default-on `explicitParallelRouteChildren` behavior
means this loader tree contains only `left` and `right`. Strict matching
keeps `/named-only/[...slug]` because both declared slots match; it does
not invent `children` and then prune the route for failing to satisfy
that nonexistent slot.

We got here incrementally.
[#47872](https://github.com/vercel/next.js/pull/47872) introduced the
404 fallback for unmatched parallel slots,
[#60186](https://github.com/vercel/next.js/pull/60186) added a
development warning because this was confusing in practice and linked
[#51805](https://github.com/vercel/next.js/issues/51805) and
[#49569](https://github.com/vercel/next.js/issues/49569), and
[#84702](https://github.com/vercel/next.js/pull/84702) later made a
missing default a build error for named slots while leaving `children`
on the old fallback for backwards compatibility. Strict matching takes
the next step and treats a matcher that can only construct a permanent
404 tree as unmatched.

This changes soft navigations that only worked by preserving a
previously active slot even though the URL could not be loaded directly,
which is why matcher pruning remains behind its own experimental flag.
The interception retention markers backed by `default-null`, including
named host slots from the first PR in this stack, are complete route
patches and are not pruned. This change however is well motivated
because if you did client nav to a route that only matches a named slot
and then hard refresh you will end up getting a 404. This is a sign our
current semantics are actually broken.

If you want to preserve the perma 404 behavior of the
slot-without-default you can just add a default and make it call
`notFound()` unconditionally.

## Verification

- `pnpm build-all`
- Focused `normalize-catchall-routes` unit coverage
- Production and development e2e coverage on Turbopack and Webpack
- The pruning e2e matrix with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 13:23:43 -07:00
Josh Story 05ed7c1be2 Omit undeclared children slots from app routes (#97184)
## Summary

Parallel route layouts can be composed entirely from named slots, but
loader tree construction currently synthesizes a `children` fallback
whenever any named slot exists. This makes `children` semantically
required even when no page, default, or ordinary route branch declares
it.

This adds `experimental.explicitParallelRouteChildren` and enables it by
default. When enabled, `children` is included in a layout slot set only
when the filesystem declares an ordinary route that can render at that
level. A layout by itself is only structure and does not declare a route
target. Ordinary descendants are traced until they reach a page or
default, including through deeper named slots, before they cause
`children` to be included. Setting the flag to `false` temporarily
restores the legacy implicit `children` fallback.

This flag only controls whether `children` exists in the loader tree. It
does not prune incomplete matchers; that is the separate
`experimental.strictRouteMatching` behavior in the next PR.

Named slots keep their existing default and soft navigation semantics.
The preceding PR retains the slots owned by an interception host without
treating `children` specially, so an undeclared child is no longer
needed for that behavior. A real `children` branch still uses the
retention marker when it is one of the host layout slots.

## Semantics

For example, this layout declares only named slots:

```text
app/dashboard/layout.tsx
app/dashboard/@left/page.tsx
app/dashboard/@right/page.tsx
```

With `explicitParallelRouteChildren` disabled, Next.js adds a synthetic
`children` branch whose built-in default calls `notFound()`, even though
the layout never declared or rendered it. With the default behavior
enabled, the loader tree contains only `left` and `right`, so
`/dashboard` is matched from the route targets that actually exist.

This is different from an ordinary branch whose route targets are deeper
in the tree:

```text
app/nested/layout.tsx
app/nested/@sidebar/[...slug]/page.tsx
app/nested/content/layout.tsx
app/nested/content/@left/[...slug]/page.tsx
app/nested/content/@right/[...slug]/page.tsx
```

Here `content` really is the `children` branch of `nested`. The scan
follows `content` through its layout and deeper named slots, so
`children` remains required. `/nested/content/anything` can construct
every declared slot, while `/nested/incomplete` only matches `sidebar`
and is still incomplete. The distinction is whether the ordinary
descendant eventually reaches a page or default, not whether a layout
happens to exist along the way.

The focused children detection coverage proves that a layout-only
descendant does not synthesize `children`, while an ordinary branch
whose route targets live inside deeper named slots still does. The
limitation coverage also proves that named-only trees render pages, CSS,
metadata, and regular error boundaries. It intentionally asserts the
current broken behavior for HTTP access fallbacks and metadata or
viewport failures so those expectations can be flipped when renderer
ownership no longer depends on `children`.

## Verification

- `pnpm build-all`
- Turbopack and webpack development and production coverage for
`explicit-parallel-route-children-detection`
- Turbopack and webpack production coverage for
`interception-dynamic-segment`, `parallel-routes-layouts`, and
`explicit-parallel-route-children-legacy`
- The same existing production coverage with Cache Components enabled
- Turbopack and webpack production coverage for the documented
named-only limitations

<!-- NEXT_JS_LLM -->
2026-08-27 12:22:31 -07:00
Josh Story cce7bf2fc0 Retain interception route host slots (#97242)
## Summary

Interception routes represent a partial update to the layout that hosts
them. Today we model this for `children` by synthesizing a `__DEFAULT__`
route backed by `default-null`, but named siblings still use normal
default matching even though they should retain their active state too.

This makes retention relative to the interception host instead of the
`children` key. Every non-intercepting sibling gets the existing
`__DEFAULT__` marker backed by `default-null`, while slots inside the
newly selected interception subtree continue to use normal matching and
real defaults.

The coverage uses a named-only host to verify that named siblings retain
client state without evaluating a user default, that a sibling without a
default also retains, and that a hard refresh still loads the canonical
route.

## Example

Consider this route tree:

```text
app/
├── named-host/
│   ├── layout.tsx                  # renders canonical, content, secondary, and modal
│   ├── @canonical/
│   │   └── page.tsx
│   ├── @content/
│   │   ├── page.tsx                # renders a stateful counter
│   │   └── default.tsx             # throws if evaluated
│   ├── @secondary/
│   │   └── page.tsx                # has no default.tsx
│   └── @modal/
│       ├── default.tsx
│       └── (..)named-target/
│           └── page.tsx            # intercepted target
└── named-target/
    └── page.tsx                    # canonical route
```

Before this change, a soft navigation from `/named-host` to
`/named-target` could replace `@content` with its throwing default and
treat `@secondary` as missing because only `children` had retain
semantics. After this change, only `@modal` switches to the intercepted
page; `@content` and `@secondary` keep their existing UI and the counter
keeps its state. A hard refresh still renders the canonical
`/named-target` route instead of the interception host.

## Verification

- Turbopack and webpack production coverage in
`interception-dynamic-segment`
- The same production coverage with Cache Components enabled

<!-- NEXT_JS_LLM -->
2026-08-27 09:27:33 -07:00
Tim Neutkens 89d017eac4 Pages Router: Deprecate React 18 support (#97689)
## Summary

- warn during `next dev` and `next build` when React 18 is installed,
while keeping `next start` quiet
- explain that React 18 remains supported in Next.js 16 but will be
removed in Next.js 17
- update the React version guidance with React 19 and TypeScript type
upgrade commands

## Verification

- `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- `NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm
test-dev-turbo
test/e2e/react-current-version/react-current-version.test.ts`
- targeted production build/start coverage passed with React 18 and
React 19
- Not passing: `pnpm --filter=next types` (existing unrelated errors in
`httpget.ts`, `image-optimizer.ts`, `proxy-request.ts`, telemetry
typings, and compiled declarations)
- Not passing: `pnpm lint` (existing unused Turbo Tasks check)

<!-- NEXT_JS_LLM -->
2026-08-27 13:00:17 +02:00
Jiwon Choi c11c48e42d Replace useSearchParams bailout error with ReactDOM.browser behind a flag (#96843)
Stacked on #96826.

This PR replaces the bailout error used by `useSearchParams` with
`ReactDOM.browser()` behind the experimental flag.

### How?

Move the bailout behind a browser-safe module boundary, use
`ReactDOM.browser()` when enabled, and preserve existing behavior
otherwise.

<!-- NEXT_JS_LLM -->
2026-08-27 02:07:26 +02:00
Jiwon Choi 23985382a7 Replace CSRBailout error with ReactDOM.browser behind a flag (#96826)
CSRBailout error is the use case for `ReactDOM.browser()`. Replace
behind an experimental flag.
This PR replaces the bailout error used in next/dynamic ssr: false.
2026-08-27 02:07:26 +02:00
Muhammad Zeeshan fad317505d fix(next/image): reject non-2xx internal image responses (#97957)
- Fixes https://github.com/vercel/next.js/issues/82357
- Closes https://github.com/vercel/next.js/pull/96985

### What?

`fetchInternalImage()` only rejects the internal response when
`statusCode` is falsy, and `MockedResponse` defaults it to `200`, so
nothing really gets rejected there. A `307` coming from a `redirects()`
entry in `next.config`, or a `404` for a path that isn't there, both
count as a successful image fetch.

That body then reaches `detectContentType()`, which returns `null`, and
all you get is:

```
The requested resource isn't a valid image for /path/to/image.png received null
```

Nothing in that line points back at the response that actually came in,
so a redirect looks exactly like a corrupt image.

### Why?

`fetchExternalImage()` right above already covers this with `if
(!res.ok)` and logs `res.status`. The internal path is the only fetch in
the file without that check, so the same failure gets reported at the
fetch for remote images, and three steps later as "not a valid image"
for local ones.

### How?

The guard now rejects anything outside the 2xx range, which is what
`res.ok` means, and the log line carries the status next to the href.
The thrown `ImageError` message stays generic, same as the external
path, so nothing new shows up in the HTTP response body.

Because that throw passes `mocked.res.statusCode` into `ImageError`, a
non-2xx internal response now reports its own status instead of always
landing on `400`. `ImageError` still maps anything under 400 to `500`,
so a redirect answers `500` and a `404` stays `404`. That is what
`fetchExternalImage()` has always done, and it lines the two up: a
missing local image now returns `404` just like a missing remote one.

Three assertions in `test/e2e/image-optimizer/util.ts` shift because of
that:

- `should not forward cookie header`: `/api/conditional-cookie` answers
`401` when the cookie is missing, so `400` becomes `401`
- `should error if the image file does not exist`: `/does_not_exist.jpg`
returns `404` now, with the internal response message
- `should error if the resource isn't a valid image`: this one requested
`/test.txt`, but the fixture in `public/` is `text.txt`, so it was
quietly exercising a missing file instead of a non-image file. Pointed
at the file that exists, it keeps its original `400` and finally tests
what its name says

Two unit tests cover the new guard, one `307` and one `404`. Every other
test in that file sets `statusCode = 200`, so they are untouched.

#96985 is open against the same report with a different angle: it
threads the status through `ImageUpstream` so the existing "isn't a
valid image" log can print it. This one stops the request at the fetch,
so the redirect never reaches the optimizer at all. Whichever one fits
better, feel free to close mine.

### Verification

- `jest test/unit/image-optimizer/`: 139/139, and the two new tests fail
against a build without the guard
- `pnpm test-dev
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `pnpm test-start
test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97
- `prettier --check` clean on the three changed files

<!-- NEXT_JS_LLM -->

Co-authored-by: Steven <steven@ceriously.com>
2026-08-26 17:04:32 -07:00
Sebastian "Sebbie" Silbermann 1a7ccf481d Re-enable AVIF image optimization (#97931)
Reverts #97875 and bumps `sharp` from `^0.35.3` to `^0.35.4`,
re-enabling AVIF image optimization.


`minimumReleaseAge` interplay (verified with pnpm 10.33.0 and 11.22.0 in
a standalone repo with a `file:` dependency declaring sharp as an
optional dependency):

- sharp 0.35.4 was published on 2026-08-26 and is younger than this
repo's 48h `minimumReleaseAge`.
- pnpm aborts the whole install with
`ERR_PNPM_NO_MATURE_MATCHING_VERSION` when no in-range release satisfies
the age gate. This applies to `optionalDependencies` (no warn-and-skip)
and through `file:` dependencies alike.
- The gate also covers the `@img/sharp-*` binary packages sharp 0.35.4
depends on, so excluding only `sharp@0.35.4` is not enough.
- `pnpm-workspace.yaml` therefore exempts `sharp@0.35.4` and
`@img/sharp-*` (pnpm does not allow version qualifiers on name patterns
in this setting). Both entries can be dropped once 0.35.4 is older than
48 hours.
- Downstream users who configure their own `minimumReleaseAge` will hit
the same hard install failure when installing a release with this bump
until 0.35.4 ages out, unless they add the same excludes. Keeping
`^0.35.3` is not a safer alternative: age-gated installs would then
silently resolve the vulnerable 0.35.3.

Part of https://linear.app/vercel/issue/VOC-34654/

---------

Co-authored-by: Steven <steven@ceriously.com>
2026-08-26 23:28:06 +02:00
Lazizbek Ergashev 0c7f81e6a3 Don't report a client-aborted RSC stream as a render error (#96715)
When a client disconnects mid-stream, `pipeNodeReadableToNodeResponse`
destroys the readable React is piping into. React sees its destination
close before the render ended and aborts with a generic `Error("The
destination stream closed early.")`, which `onError` has no way to
distinguish from a real render error, so it ends up in `onRequestError`
as a server render error.

The Web streams path doesn't have this problem: cancelling the readable
passes the `ResponseAborted` reason straight through to React. This does
the same for the Node path by aborting the pipeable with that reason as
soon as the passthrough is destroyed early, before React's own close
handler runs. Genuine render errors still go through untouched.

Fixes #96704

---------

Co-authored-by: Jude Gao <jude.gao@vercel.com>
2026-08-26 16:26:01 -04:00
Janka Uryga d352f1740a [PPF] Only track runtime accesses when the promise is used (#97165)
Improves how we track accesses of runtime data in static prerenders
(which affects whether a page will use static or runtime shells).
Now, Instead of tracking it when the hanging promise is created, we'll
track it when `then/catch/finally` is called on the promise (which we
use as a signal for being used).

The promise tracking required some novel work, because:

- Using a proxy seems to cause `Promise.then` to show up in call stacks
(we've had issues with proxies throwing off React's IO tracking before,
i believe this is a similar problem)
- patching `then/catch/finally` on the promise directly doesn't work,
because `then` doesn't get called for native `await`

The only way of doing this that i've found is a custom `Promise`
subclass that tracks the access in its `then` implementation. This is
what `TrackedPromise` does.

To improve the debuggability, i've added some debug logs enabled by
`NEXT_PRIVATE_DEBUG_RUNTIME_DATA=1` to detect when a page is deopted to
runtime and what expression caused it.

---

This fixes a false deopt caused by `createServerPathnameForMetadata` -
when called on a route with fallback params, it would always track a
fallback params access even if no icons/og images that actually use it
exist. This meant that we'd never statically optimize any routes that
had fallback params. After this PR, we only track a runtime data access
if a metadata route that uses that params actually exists, in which case
the `pathname` promise gets awaited.
(as a future optimization, we should do the equivalent of vary params
tracking on metadata routes to know if the params are used at all. also,
the logic in `pathname.ts` is generally in need of cleanup)
2026-08-26 18:05:44 +02:00
Andrew Clark 94327e8ade Port React's @gate test directive to the e2e harness (#96228)
The test suite has accumulated a bunch of patterns for disabling tests
that are known to fail under some configuration: `it.skip`, `if
(isNextDev) { test('skipped in dev mode', () => {}); return }`, whole
describes toggled off by checking `process.env.__NEXT_CACHE_COMPONENTS`.
These all have the same flaw: nothing tells you when the thing you
skipped starts working. The test stays disabled forever, and the
workaround it was guarding rots along with it.

React solves this with the `@gate` pragma, and this PR ports it to the
Next.js e2e harness:

```ts
// Blocked on the optimization that marks a route as fully static when
// no dynamic params are referenced in Server Components.
// @gate !cacheComponents
it('navigates to a page with a lazily-generated static param', async () => {
  // body unchanged
})
```

The test still runs. If the condition is false and the test fails, the
failure is absorbed and the suite stays green. If it _passes_, the suite
fails: the gate is stale, delete it. So instead of a skip that hides a
fixed bug indefinitely, you get a CI failure the day the fix lands.

When the condition is static, the inversion is Jest's own `test.failing`
under the hood. A lazy condition isn't known until the fixture's
resolved config is read inside the body, so those tests invert at
runtime instead.

`// @force-gate <condition>` skips for real — for tests that can't even
be attempted (prefetching is disabled in dev, deploy has no local build
output, the fixture won't build under the condition), and for tests of a
new API, where the disabled state can only throw and running it proves
nothing:

```ts
// Prefetching is disabled in dev, so this suite has nothing to test.
// @force-gate prefetching
describe('segment cache prefetch scheduling', () => {
  // ...
})
```

There's no staleness check in that case, so this is a judgment call:
prefer `@gate` when the off state fails for a meaningful reason — the
flag changes behavior that already exists — and `@force-gate` when the
body can only throw because the API doesn't exist. A static condition
(mode, bundler) resolves at collection time into a normal Jest skip. A
lazy condition resolves at runtime, and when a lazy force-gate on a
describe is false, we skip the fixture build entirely — that's what
makes it usable for suites whose fixtures are build-incompatible with
the condition. (One caveat: Jest has no way to skip a test that's
already running, so these report as passing with a warning in the log,
not as skipped.)

Conditions live in a hand-written registry. I considered deriving the
lazy ones from the config schema automatically, but a gate is a claim
about which dimension of the test matrix explains a failure, and I'd
rather each of those claims be spelled out with a description.
Referencing an undeclared name fails the suite at collection time, so a
typo can't silently disable a gate.

The important design decision for lazy conditions is that they read the
fixture's _resolved_ config, never `process.env`. The env var isn't the
truth: `__NEXT_CACHE_COMPONENTS=true` only applies when the fixture
doesn't set `cacheComponents` itself, and config resolution implies
flags the fixture never mentions (`cacheComponents: true` alone turns on
`experimental.ppr`). Resolution happens in a child process, because
in-process `loadConfig` would leak the fixture's `.env` files into the
Jest worker. Suites with no lazy gate never pay for any of this.

The condition expression is parsed using a small grammar (also ported
from the React repo). An expression that doesn't parse fails the suite:

```ts
// @gate mode === 'start' && !cacheComponents
// @gate !(turbopack || rspack)
```

There's also a runtime version, mirroring React's `gate(flags =>
flags.enableFoo)`, for tests that run under both states but assert
differently (and for `it.each`, where the pragma can't attach):

```ts
import { gate } from 'next-test-utils'

it('renders the fallback', async () => {
  if (await gate((conditions) => conditions.cacheComponents)) {
    // PPR: the fallback is part of the static shell
  } else {
    // fully dynamic: the fallback streams in
  }
})
```

It also accepts the pragma expression language as a string: `await
gate('cacheComponents && !dev')`.

Docs are in `test/lib/gate/README.md`; `test/unit/gate/` covers the
transform, the expression language, and the runtime.
2026-08-26 10:22:11 -04:00
Tobias Koppers 43d81b9d55 feat(turbopack): resolve /-rooted imports from the project directory (#97799)
### What?

A request starting with `/` — `import '/content/where'`,
`require('/foo.js')` — now resolves from
the **project directory** (the one holding `next.config`), and cannot
reach outside it. Previously
it resolved from the root of the filesystem while reporting *"server
relative imports are not
implemented yet"*.

This is the base of a stack: the follow-up on top of it moves
`import.meta.glob`'s `/`-rooted
patterns onto the same root, so a pattern and a plain import agree on
what `/` means.

### Why?

The feature was half-present and mislabelled. The
`Request::ServerRelative` arm rewrote `/foo` to
`./foo` and resolved it against the filesystem root, then emitted a "not
implemented yet" issue
unless an import-map alias had already matched. Users got a diagnostic
telling them to give up on
something that in fact resolved.

Worse, resolving from the *filesystem* root means `/` escapes the
project. In a Turbopack execution
test — where the filesystem root is the repository root —
`require('/package.json')` resolved this
repository's own `package.json`. In a monorepo, where Turbopack's root
is the workspace root and the
app lives in `apps/web`, `/content/x.js` would reach a workspace-level
file rather than the app's.

The intended semantics are Vite's, and they were confirmed by
measurement rather than from memory.
In a workspace where `content/*.js` exists *both* at the workspace root
and under the Vite root, so
the two are distinguishable, Vite 5.4.21:

- resolves `/content/…` from the configured `root` — changing `root`
changes which file wins;
- fails outright for a file that exists only above `root`, rather than
walking up;
- behaves identically for a plain `import` and for `import.meta.glob`.

### How?

`ResolveOptions` gains `server_relative_root`, the directory a
`/`-rooted request resolves from, and
`ResolveOptionsContext` exposes it so embedders can set it. Next.js sets
it to the project directory
in the client, server and edge resolve contexts — all of which already
had it to hand, so nothing new
is threaded through.

Three deliberate choices:

- **The option is optional and defaults to the previous behaviour.**
`turbopack-core` is used well
beyond Next.js; an embedder that doesn't set it keeps resolving `/` from
the filesystem root. This
is also why the change is provably contained — the whole
`turbopack-tests` suite passes with no
  snapshot churn.
- **It is named and documented in filesystem terms**, tied to the
existing `Request::ServerRelative`
vocabulary, rather than introducing a "project root" concept into
`turbopack-core`, which must stay
  free of Next.js concepts.
- **There is no fallback.** Resolution happens in the configured root
and stops; a request that isn't
there is a normal module-not-found. A fallback to the filesystem root
would reintroduce exactly the
  escape this fixes.

The "not implemented yet" issue is removed. The equivalent issue for
Windows-style absolute requests
is untouched, as is the separate tsconfig `extends` handling, which
treats a rooted path as absolute
per TypeScript's own rule.

### Testing

The Turbopack execution harness already has two distinct roots — the
repository root as the
filesystem root, and each test's own directory — so pointing the new
option at the test directory
makes "project directory" and "filesystem root" genuinely different, and
the distinction testable
without a Next.js app. Two fixtures: one where a `/`-rooted import and
require resolve from the
project directory, and one asserting that `/package.json`, which exists
only at the filesystem root,
does *not* resolve. The former emits no issues at all, which is what
pins the removal of the
diagnostic.

The end-to-end case uses the existing monorepo fixture, where the
project directory really is not the
filesystem root. A file of the same name exists in both
`apps/web/content` and the workspace root, so
the rendered value alone identifies which root was used — a test that
merely asserted "it renders"
would have passed under the old behaviour too.

### Known gap

TypeScript resolves a leading `/` as an absolute path on disk, so a
`/`-rooted import in a `.ts` file
still fails type checking (`TS2307`) even though it bundles and runs.
The e2e page is therefore
JavaScript. A tsconfig `paths` mapping works around it for users, but it
also makes the request match
the import map instead, so the two resolvers overlap; that deserves its
own decision and is tracked
separately rather than papered over here.

<!-- NEXT_JS_LLM -->

<!-- fleet 3acc7e63-52a8-4472-89cd-b1bbb8ad59fa -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
2026-08-25 21:46:47 +02:00
Sebastian "Sebbie" Silbermann 09f9c8a758 Fix ISR misses with backslashes in segments when deployed on Windows (#97876)
https://github.com/vercel/next-js-mirror/pull/150
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 18:12:50 +02:00
Sebastian "Sebbie" Silbermann 7a5937a8ab [next/image]: disable avif image optimization (#97875)
https://github.com/vercel/next-js-mirror/pull/160
2026-08-25 18:12:50 +02:00
Hendrik Liebau dae438dc0d Stop printing a stack frame for error message text (#97829)
Next.js printed a stack frame that does not exist for an error whose
message ends with text that looks like a file location. A logged
connection failure read `at <unknown> (Error: connect ECONNREFUSED
::1:45999)` in front of its real frames.

`stacktrace-parser` tries five patterns on every line, and the pattern
for JavaScriptCore makes the `<name>@` prefix optional. That pattern
`:<line>:<column>`, and a line of an error message can end that way. A
JavaScriptCore frame always holds `@`, also at the top level, so the
prefix is not optional in a real stack.

The patch for `stacktrace-parser` now requires that prefix. A line of a
message no longer becomes a frame, and every JavaScriptCore frame still
parses, including one whose path holds a space.

A Turbopack build error puts the file and the position of the error in
its message, and the parser read that line as a frame as well. That
frame is gone now, and the location stays in the message. The snapshots
of the build errors record the change.
2026-08-25 13:38:47 +02:00
Hendrik Liebau bf07db0713 Show the errors of an AggregateError behind a cause (#97830)
A failed `fetch` hid the reason of the failure. Node.js reports every
refused connection attempt in the `errors` of an `AggregateError`, and
the terminal logged `[errors]: [ [Error], [Error] ]` without the address
or the port of a single attempt.

An `AggregateError` keeps its errors in an array. Node.js spends one
level of the inspect depth on that array, and one more level on each
error in the array, while a `cause` costs one level only. The errors of
an `AggregateError` behind a cause therefore fell outside the default
depth of two. Node.js also returns an error early when it carries no own
properties, before it applies the depth, so the collapse hit only the
errors that carry diagnostics.

Next.js now adds the missing level back for an `AggregateError`, so its
errors print at the same nesting as a cause. A depth of `null` means
unlimited and stays unchanged, because `null + 1` would reduce it to one
level.
2026-08-25 10:07:55 +02:00
Marcos Hernanz 95b77e8279 Fix metadata prefetch cache key for search params (#97729)
### What?

Treat a `LoadingBoundary` response as `Full` when writing the response's
head into the segment cache, matching the strategy already used when the
scheduler creates that head entry.

Adds a production e2e regression test with two concurrent prefetches for
the same pathname and different search params.

### Why?

`pingRuntimeHead` creates loading-boundary head entries with the
concrete `Full` strategy because a head has no loading boundary. The
canonical re-keying added in #96095 later wrote the fulfilled head with
raw `LoadingBoundary` semantics, which drops search params from the
metadata cache key. Concurrent query-specific metadata responses could
therefore replace each other under one fallback key; whichever response
settled last supplied the title for both URLs.

Fixes #97657

### How?

Normalize `LoadingBoundary` to `Full` at the head write boundary only.
Page segments retain loading-boundary keying, and shell/PPR responses
keep their existing strategies.

The test reveals alpha and beta links in one React render, waits for
both prefetches, navigates to alpha, and verifies both body and title.
It then performs a search-only navigation to an unprefetched gamma URL
and verifies that the head is replaced.

### Testing

- Regression boundary: test passes at `e70495c1ee` and fails at #96095
merge commit `dfa7f4f713` without the fix (`alpha` body, `beta` title).
- Test passes at `dfa7f4f713` with the fix.
- `NEXT_SKIP_ISOLATE=1 pnpm test-start-turbo
test/e2e/app-dir/segment-cache/metadata-search-params/metadata-search-params.test.ts`
(pass, plus 3 consecutive repeat runs)
- `NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack
test/e2e/app-dir/segment-cache/metadata-search-params/metadata-search-params.test.ts`
(pass)
- Relevant existing Turbopack suites: 16 tests pass across metadata,
static shell, metadata soft navigation, and stale search-param coverage.
- Existing search-param suites: 6 tests pass with webpack. Their
Turbopack builds are currently blocked on canary by an unrelated Edge
Runtime `process.cwd()` error.
- `pnpm --filter next build`
- Targeted ESLint and Prettier checks
- Structured external review: 0 findings
2026-08-24 14:13:21 -07:00
Niklas Mischkulnig 6617057757 Turbopack: emit/collect (#96631)
Only add this for using it for Server Actions for now
Undocumented, not API stable. But we can't really put it behind a flag,
because the next-custom-transform is going to add it

New traits:
```rust
/// A module that can collect other modules during the collect phase.
#[turbo_tasks::value_trait]
pub trait CollectingModule: Module {
    /// The namespace that this module is interesed in
    #[turbo_tasks::function]
    fn namespace(self: Vc<Self>) -> Vc<RcStr>;

    #[turbo_tasks::function]
    fn as_chunk_item(
        self: Vc<Self>,
        module_graph: Vc<ModuleGraph>,
        chunking_context: Vc<Box<dyn ChunkingContext>>,
        entry_chunk_group: Vc<Modules>,
    ) -> Vc<Box<dyn ChunkItem>>;
}

#[turbo_tasks::value_trait]
pub trait EmittedModuleReference: ModuleReference + ValueToString {
    #[turbo_tasks::function]
    fn data(self: Vc<Self>) -> Vc<CompileTimeDefineValue>;
}

```

Example:

```javascript
__turbopack_emit__('./c.js', {
  namespace: 'my-test',
  data: 'data-for-c', // optional
  exports: ['c'], // optional
  with: ... // optional, import attributes like with ESM
})

// -------------------------

const getList = __turbopack_collect__({
  namespace: 'my-test',
})
const list = getList();
```

Shortcomings in the current version (which are not a blocker for now
however):
- data-only emit not implemented
- stylegroups aren't properly implemented. emitting styles is probably
broken
- pages router is still a problem: it has two separate
ChunkGroup::Entry, so you only see what was emitted on the current
runtime, not both

Recreation of https://github.com/vercel/next.js/pull/91100
2026-08-24 18:39:28 +02:00
MB c0dfeffa53 Add next/cache-handlers types entrypoint (#97592)
### What?

Exposes the `cacheHandlers` types (`CacheHandler`, `CacheEntry`) as
types-only exports from `next/cache`, so handlers can be checked against
the real interface:

```ts
import type { CacheHandler } from 'next/cache'
```

### Why?

Custom cache handler authors currently import `CacheHandler` and
`CacheEntry` from `next/dist/server/lib/cache-handlers/types` (an
internal path that can move between versions) or hand-copy the
interfaces, which drift silently across releases.

### Motivation

I'm working on a custom community cache handler and it'd be great to
import the types directly and keep testing against the source of truth,
rather than maintaining a hand-copied mirror that has to be re-checked
on every Next.js release.

Raised in discussion #96356.

### How?

- Adds a type-only `export type { CacheHandler, CacheEntry }` to
`packages/next/cache.d.ts`, re-exported from
`./dist/server/lib/cache-handlers/types`. No new subpath or
`package.json` `"files"` entries needed since `next/cache` already
ships.
- Docs: `cacheHandlers.mdx` now shows the public `next/cache` import
instead of the GitHub source links.
- Tests: a `satisfies CacheHandler` / `satisfies CacheEntry` fixture in
the `typescript-basic` typechecking suite (runs `tsc` against the
installed package), and the `use-cache-custom-handler` e2e fixture's
JSDoc now uses the public import.

(Originally proposed as a separate `next/cache-handlers` types-only
subpath, following the `next/types` pattern moved the export into
`next/cache` per review.)

Related: #96356

closes #97781 (only created to run deploy tests)
2026-08-24 13:14:31 +02:00
Tobias Koppers cf5339d1be test: deflake basePath external navigation (#97611)
## Summary

The basePath external-navigation test depended on loading Google and
recognizing its DOM before exercising browser history. That made the
test sensitive to public network availability and third-party page
changes; the reported failure was confirmed in the Safari/WebKit CI run
on the initial attempt and both retries.

For local and CI test-server runs, this replaces the public dependency
with a minimal page served from a separate local origin, preserving the
cross-origin, full-document navigation being tested. Deploy mode remains
enabled and uses a deploy-accessible external target because deployed
browsers cannot reach the test runner. Both paths now synchronize on
browser origin and exact restored-page state instead of third-party DOM
or a fixed sleep.

## Verification

- Focused production/start-mode test passed three consecutive times in
Firefox
- Focused production/start-mode test passed in Chromium
- Prettier and ESLint passed for the changed files
- `pnpm build-all`
- Not run: Safari/WebKit locally (Playwright’s unsupported Amazon Linux
fallback requires incompatible Ubuntu system libraries; the Safari CI
job is the definitive validation)
- Not run: deploy-mode execution locally (deployment credentials
unavailable; the deploy path remains enabled for CI)

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-24 11:41:05 +02:00
Jiwon Choi d7aa66c345 Remove generated error codes (#97687)
### Why?

Should come up with better solution that does not block PRs with git
conflict

x-ref:
https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH

### How?

- Delete `errors.json`, the error-code SWC plugin, generated WASM, merge
driver, and validation/build tooling.
- Stop attaching error codes to server-rendering digests, redboxes, and
telemetry; native `Error.code` and `Error.name` remain available where
applicable.
- Remove the development-overlay error feedback UI, middleware, and
telemetry event that depended on stable codes.
- Update fixtures, snapshots, and guidance for code-free errors and
numeric-only digests.

<!-- NEXT_JS_LLM -->
2026-08-21 22:45:12 +02:00
Janka Uryga 43273a1d21 [PPF] Instant validation for unstable_navigation() (#97309)
Adds discriminated error messages for `unstable_navigation()` used
outside of Suspense.

Previously we only had to discriminate two kinds of errors in each
validation flow:
- App Shell -- 1. link data (if it resolves in `Runtime`) or 2. dynamic
data (if it resolves in `Dynamic`)
- Static Shell -- 1. runtime data (if it resolves in `Runtime`) or 2.
dynamic data (if it resolves in `Dynamic`)

However, with `navigation()` (96908), the App Shell flow needs to
distinguish three kinds: 1. link data, 2. navigation (if it resolves in
`NavigationRuntime`), and 3. dynamic data. This complicates the
validation code a bit, because we can no longer get away with one retry.
To accommodate this, i restructured the validation code to no longer
recurse as a method of retrying. We just loop over an array that defines
what order we should try the stages in + what kind of hole shows up in
each stage.

I've also removed `hasAmbiguousErrors` and the associated logic. since
we're no longer mixing Static+Runtime segments in a runtime prefetch,
this is no longer relevant -- all segments used for validation use one
stage, so whether or not the error is ambiguous only depends on the
stage.

For ease of reviewing, this is split into two commits:
1. updating the (many) places that need a new `DynamicHoleKind` in our
error messages. This one has a lot of pretty mechanical changes, and was
also machine-reviewed for consistency already, but the messages
themselves are worth looking at (we'll also need to update docs --
leaving that for a follow-up)
2. the actual instant validation changes, i.e. using the new
`DynamicHoleKind` and adding the new pass
2026-08-21 21:08:38 +02:00
Niklas Mischkulnig 8482890aca test: make app-document-import-order more robust (#97682)
- The `brower.eval(callback)` version was a bit weird, use
beforePageLoad as we do everywhere else
- Properly parse the URL and match against the pathname, to not break
with `?dpl`
2026-08-21 17:37:46 +02:00
David Alexandru Ilie ba9f073cc2 Add a Turbopack error for missing root layouts (#97639)
## Summary

Turbopack currently builds an App Router page without a root layout,
leaving the problem to surface later at runtime. Report it while
building the app structure instead:

`route/page.js doesn't have a root layout. To fix this error, make sure
every page has a root layout.`

The check uses the layouts already inherited during directory-tree
traversal, so route groups and apps with multiple root layouts keep
working. Route handlers are unaffected.

Webpack behavior is intentionally unchanged in this PR.

## Verification

- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/create-root-layout/create-root-layout.test.ts`
- `HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/create-root-layout/create-root-layout.test.ts`
- `HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/create-root-layout/create-root-layout.test.ts`
- `HEADLESS=true pnpm test-start-webpack
test/e2e/app-dir/create-root-layout/create-root-layout.test.ts`
- `HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/root-layout/root-layout.test.ts`
2026-08-21 15:07:42 +01:00
Janka Uryga 3cdb56c251 [PPF] unstable_prefetch() (#97622)
Implements `unstable_prefetch()`, which is intended for use in
`partialPrefetching`. `await unstable_prefetch()` excludes content from
the app shell -- it will only be available when using `prefetch={true}`
(speculative prefetch) or during navigations.

As a rule of thumb, `unstable_prefetch()` resolves whenever static
`params` would:
- in a static prerender
  - but NOT the app shell extracted from it, which is param-less
- in a runtime prefetch (`prefetch={true}`)
  - but NOT a runtime app shell, which is param-less

Note that `prefetch()` is URL data, so using it in an App Shell without
Suspense will trigger an instant insight.

### Implementation notes

`prefetch()` is treated like URL data, so it resolves in the
`PrefetchStatic/PrefetchRuntime` stages added in #96908. The
implementation is basically analogous to `unstable_navigation()` except
using different stages. i've considered abstracting them into one
implementation, but decided against that for now, we can deduplicate
later.

Error messages about URL data have not been updated to mention it yet --
we will do that as a follow up, along with docs.

`await prefetch()` does not count as a runtime data access, meaning that
it won't affect the static prefetch hint for a route. however `await
prefetch(); await cookies()` does deopt the route, because using a
speculative runtime prefetch would reveal more content. Note that this
may cause us to unnecessarily deopt a shell to runtime even if only the
speculative part of the content would be improved by a runtime request;
this is not a new issue, but it's something we should optimize.
2026-08-21 13:50:23 +02:00
Niklas Mischkulnig 6c0dd8400b Turbopack: deduplicate Pages Router app chunks (#97664)
Previously, Turbopack treated `_app` and Pages router pages as
completely separate.

So using code both in `_app` and `pages/foo.tsx` would lead to a lot of
duplicated code to be loaded at runtime.
But in reality, `_app` is always loaded for Pages, so we can thread the
availability info and skip chunking modules that were already loaded by
`_app`.

Recreation of https://github.com/vercel/next.js/pull/97549

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
2026-08-21 11:12:53 +02:00
WangTao 14a69ef78b fix: preserve trailing slash during export MPA fallback (#97613)
Keeps the trailing slash when a static export falls back to a full page
load, so you land on the canonical URL instead of bouncing through a
redirect.

Fixes https://github.com/vercel/next.js/issues/97608

---------

Co-authored-by: Jude Gao <jude.gao@vercel.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:15:22 -04:00
Janka Uryga e3a78fd3c1 refactor: move useDynamic{Route,Search}Params to reduce snapshot churn (#97360)
Relocates `useDynamicRouteParams` and `useDynamicSearchParams` to avoid
having to update `client-hook-abort-reasons.test.ts` every time there's
changes in `dynamic-rendering.ts`. Webpack leaked the source location of
the `React.use` calls inside, so any code change that moved the source
of the hook would require a snapshot update.

For reasons i don't quite understand, moving the hooks to a different
file improved the ignore-listing, i.e. we no longer point to the
internals of the hooks. This is surprising but not unwelcome.
2026-08-20 15:27:12 +02:00