Commit Graph

1455 Commits

Author SHA1 Message Date
github-actions[bot] 365c604032 chore: version packages (beta) (#2844)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 09:49:29 +00:00
James Anderson 3ec5bdc68a fix(build): support Pages Router ESM externals (#2877)
* fix(build): support Pages ESM externals

* fix(build): isolate Pages ESM external ownership

* fix(build): track aliased dynamic Pages imports

* fix(build): track static template Pages imports

* fix(dev): keep Pages ESM dependencies internal

* test(build): serialize ESM externals coverage

* perf(build): skip Pages ownership in App-only builds

* refactor(build): split process.browser compatibility

* fix(build): preserve server conditions in Workers

* fix(build): preserve server dependency conditions

* test(worker): cover ESM externals parity in workerd

* perf(build): omit Pages externals from App-only graphs

* test(worker): preserve ESM externals tripwires

* test(build): cover adapter-independent server conditions

* test(build): find renamed server conditions plugin

* test(worker): isolate ESM package coverage

* test(build): use checked-in ESM externals fixture
2026-08-15 02:33:17 +01:00
James Anderson 965750bbd7 fix(build): preserve server export conditions (#2918)
* fix(build): preserve server conditions on Workers

* fix(build): filter browser from all server graphs

* refactor(build): rename server conditions plugin
2026-08-15 02:33:16 +01:00
MaxtuneLee 88ea15a05d fix(headers): preserve config Link headers alongside React preload links (#2791)
* fix(headers): preserve config Link headers alongside React preload links

* fix: vp check error

* fix(headers): preserve Link header precedence

* test(headers): expect framework Link after middleware

* fix(headers): preserve Link provenance across response paths

---------

Co-authored-by: James <james@eli.cx>
2026-08-15 00:26:10 +01:00
Elias Shieh d1fb2ebf53 fix(pages): keep charset first in custom document heads (#2909)
* fix(pages): keep charset first in custom document heads

* fix(pages): preserve complete head ordering parity

* test(pages): assert dev document head ordering

* fix(pages): preserve head order across Fast Refresh

* fix(pages): align trace metadata head ordering in dev

* fix(pages): match Next head manager updates

* fix(pages): order unknown head tags last

---------

Co-authored-by: James <james@eli.cx>
2026-08-15 00:20:37 +01:00
James Anderson a3d1d1d18f fix(build): bundle image-size in vinext (#2913) 2026-08-14 10:16:05 +01:00
Nathan Nguyen ee1d8571f8 fix(app-router): reject unverified Worker prerender params (#2901) 2026-08-13 20:34:55 +01:00
Norcleeh 98fd69cc6f fix(font): not emitting 400 as default value when font weight is omitted (#2796) 2026-08-13 20:14:07 +01:00
James Anderson 92c57b992b fix(build): define process.browser per environment (#2899)
* fix(build): define process.browser per environment

* fix(build): keep environment condition filter linear

* perf(build): narrow environment condition filtering

* perf(build): filter process browser syntax locally
2026-08-13 11:49:06 +01:00
Nathan Nguyen fefc8e21d1 fix(app-router): preserve request.cf in route handlers (#2886)
* fix(app-router): preserve request.cf in route handlers

Cloudflare Workers attaches request.cf at the inbound request boundary, but App Router dispatch rebuilt Route Handler requests with the standard Request constructor and discarded that runtime metadata.\n\nUse the existing metadata-preserving URL clone boundary so both Node and Edge Route Handlers retain cf while keeping their current URL normalization semantics.

* fix(app-router): preserve request.cf through runtime wrapping

Route Handler request tracking can rebuild an already-normalized request when restoring basePath or applying middleware header overrides. Those reconstructions discarded Cloudflare metadata before the final NextRequest reached user code.\n\nPreserve cf at both reconstruction points while retaining the existing body-transfer and RequestInit behavior, with runtime-level coverage for each path.

* fix(app-router): treat request.cf as dynamic state

Preserving Cloudflare request metadata makes request.cf observable to Route Handlers, but the request proxy did not classify reads as dynamic. Geo-dependent responses could therefore enter the route-wide ISR cache, while static route modes exposed request-specific metadata.\n\nApply the existing ip and geo policy to cf: track reads in automatic mode, hide it under force-static, and reject it under dynamic error mode.

* fix(app-router): preserve request.cf across clones

Tracked Route Handler requests inherit the standard Request clone implementation, which omits Cloudflare request metadata. A handler that cloned before reading cf therefore lost the metadata even though the original tracked request retained it.\n\nCentralize tracked request cloning so cf is reattached before the clone is recursively wrapped with the same dynamic request policy.

* fix(app-router): track reflective request.cf access

Tracked Route Handler requests enforced dynamic policy only through the Proxy get trap. Descriptor, membership, and key-enumeration reads could expose Cloudflare metadata without marking the handler dynamic or respecting static modes.

Route explicit cf reflection through the same policy, filtering configurable cf keys under force-static and rejecting reflective access under dynamic error mode.

* fix(app-router): retain request proxy through valueOf

Binding every Request method to the underlying NextRequest let valueOf return the raw target. Subsequent request.cf reads could then bypass dynamic tracking and static-mode policy.

Bind valueOf to the proxy receiver while leaving branded Request methods on the underlying target, and cover the escape in all three request modes.

* fix(app-router): bind reflection to request proxy

Inherited Object reflection helpers were still bound to the raw NextRequest, allowing ownership checks to bypass request.cf dynamic policy.

Bind exact Object.prototype methods to the proxy receiver while preserving the branded target for Web Request methods, and cover ownership and enumerability checks in every request mode.

* fix(app-router): preserve proxy for request extensions

Binding unknown request properties to the raw NextRequest let user-defined methods and getters bypass request.cf policy.

Snapshot the runtime's built-in Request surface for branded target access while keeping own and unknown extensions on the proxy receiver. Cover both method and accessor escapes in every request mode.

* fix(app-router): hide cf before locking requests

Force-static proxies filtered request.cf from reflective operations, but a non-extensible target made that omission violate Proxy invariants.

Remove the configurable metadata before preventing extensions, and cover preventExtensions, seal, and freeze while preserving force-static policy.

* fix(app-router): bind Worker request members correctly

Cloudflare may expose Web IDL Request members as own properties, so classifying every own member as a user extension caused illegal invocation errors in Workers.

Use the captured built-in API surface regardless of property placement, while keeping unknown extension names on the proxy receiver. Add coverage for an own branded accessor.

* fix(app-router): distinguish request member shadows

Name-only branded member detection fixed Workers own-property layouts but treated post-wrap user shadows as runtime Web IDL members, allowing their this-based reads to escape the proxy.

Snapshot runtime-owned descriptors before route code receives the request and bind only unchanged implementations to the target. Later shadows retain the proxy receiver in every request mode.

* fix(app-router): detect request prototype shadows

Own-descriptor snapshots still treated replaced inherited Request members as branded, allowing prototype getters to bypass request.cf policy.

Snapshot each resolved built-in implementation and compare the currently resolved descriptor before selecting the raw target. Own and prototype shadows now retain the tracked receiver.

* fix(app-router): snapshot request built-ins before routes

* fix(app-router): preserve branded Worker request methods

* fix(app-router): preserve reflected cf after locking

* refactor(app-router): contain request.cf policy on its target

Route handlers need Workers metadata to follow static-generation policy without changing the semantics of every Request property. Use the configurable cf accessor copied onto NextRequest as the single policy boundary, and delegate request reconstruction to the canonical clone helpers.

* fix(app-router): avoid synthesizing absent request.cf

Ordinary requests should not gain an own cf property merely because route-handler dynamic tracking is active. Install the target accessor only when Workers metadata exists, while keeping direct absent reads subject to automatic, force-static, and error policy.

* fix(app-router): harden request.cf tracking

* chore(test): register worker route fixtures

* chore(app-router): deduplicate request.cf descriptor

---------

Co-authored-by: James <james@eli.cx>
2026-08-13 11:39:47 +01:00
Norcleeh eb092be208 fix(use-cache): allow literal exports from use-cache files (#2906)
* fix(use-cache): allow literal exports from use-cache files (#2896)

* fix(use-cache): preserve class export validation

* fix(use-cache): reject default literal exports

---------

Co-authored-by: James <james@eli.cx>
2026-08-13 10:19:06 +01:00
James Anderson 486653bdb5 fix(router): preserve basePath in Pages Router events (#2888)
* fix(router): preserve basePath in router events

* fix(router): align popstate event timing

* fix(router): cancel superseded navigation work

* fix(router): defer history until route load

* fix(router): preserve redirect destination state

* fix(router): normalize followed redirect paths

* fix(router): pin double-slash redirect fetches

* fix(router): normalize relative redirect origins

* test(router): await popstate completion

* docs(router): clarify internal redirect flow

* fix(router): follow redirects reached by popstate

* fix(router): preserve query-only rewrite history

* fix(router): preserve middleware hard-navigation history
2026-08-13 10:17:42 +01:00
yyh 848f413a9d fix(rsc): preserve BOM bytes in embedded Flight chunks (#2905) 2026-08-13 09:27:25 +01:00
James Anderson c7d5e7bd48 fix(cache): preserve binary fetch response bodies (#2907) 2026-08-13 09:15:27 +01:00
Norcleeh 0440f5a765 fix(cache): ISR cache should store the framework preload header (#2900)
The post-middleware `Link` header, instead of the framework
preload header, is mistakenly stored into the ISR cache.
2026-08-12 14:59:02 +01:00
James Anderson bdc7703b5e fix(pages): align middleware rewrite navigation (#2891)
* fix(pages): align middleware rewrite navigation

* fix(pages): classify localized route queries

* fix(pages): close middleware review gaps

* fix(router): preserve dynamic params in rewrite queries

* fix(link): preserve app prefetch in hybrid routes
2026-08-12 13:38:45 +01:00
James Anderson 09f6a0d090 fix(build): preserve transitive external versions (#2887)
* fix(build): preserve transitive external versions

* fix(build): cover default transitive externals

* perf(build): filter transitive external resolution

* chore(build): keep resolver helpers private

* fix(build): canonicalize transitive external ids

* fix(build): scope transitive resolution to builds

* fix(build): resolve queried transitive importers
2026-08-12 12:35:18 +01:00
James Anderson bc97bf68eb fix(app-router): preserve valued RSC queries through routing (#2883) 2026-08-11 17:40:07 +01:00
James Anderson f48aeb5848 fix(app-router): bail out static search params rendering (#2882)
* fix(app-router): bail out static search params rendering

* fix(app-router): preserve PPR search params tracking

* test(app-router): allow streamed search params fallback
2026-08-11 14:19:48 +01:00
James Anderson 05f2ac6824 fix(app-router): expose not-found fallback flight payload (#2349)
* fix(app-router): expose not-found fallback flight payload

* fix(app-router): release mirrored fallback flight data

* fix(app-router): clear progressive fallback flight mirrors

* fix(app-router): preserve deferred flight consumers

* test(app-router): avoid parsing script tags with regex

* test(app-router): refresh flight transform options
2026-08-11 14:15:18 +01:00
James Anderson 7363ba9312 fix(config): preserve native TypeScript dynamic imports (#2353)
* fix(config): preserve native TypeScript dynamic imports

* fix(config): retain CJS fallback for native configs

* fix(config): detect native CJS references precisely

* test(config): cover CJS native loader fallback

* fix(config): harden native CJS detection

* fix(config): resolve async configs inside runner
2026-08-11 14:09:31 +01:00
James Anderson 8c1804d875 fix(app-router): prefetch root-param segment trees (#2856)
* fix(app-router): prefetch root-param segment trees

* fix(app-router): scope root-param segment prefetches

* fix(app-router): preserve route-tree prefetch freshness

* refactor(app-router): share route-tree prefetch gate
2026-08-10 23:11:19 +01:00
James Anderson c4d645b196 fix(prerender): cache use-cache metadata routes (#2848)
* fix(prerender): expose production build phase

* fix(prerender): load config in production build phase

* fix(prerender): cache use-cache metadata routes

* test(ci): serialize route collision fixture

* fix(prerender): skip uncached metadata seeds
2026-08-10 23:04:04 +01:00
James Anderson b39f499487 fix(cache): vary use cache entries by root params (#2847)
* fix(cache): vary use cache entries by root params

* fix(cache): retain context through cache serialization
2026-08-10 21:59:33 +01:00
James Anderson 4e95e97c5c fix(cache): keep encoded dynamic prefetches learning-only (#2866)
* fix(cache): keep encoded dynamic prefetches learning-only

* fix(cache): refresh expired router prefetches

* fix(cache): preserve static prefetch paths
2026-08-10 18:41:00 +01:00
James Anderson acc2b202a2 fix(prerender): expose the production build phase (#2846)
* fix(prerender): expose production build phase

* fix(prerender): load config in production build phase
2026-08-10 16:50:17 +01:00
James Anderson 056cd026b3 fix(app-router): preserve full prefetch stale windows (#2851)
* fix(app-router): preserve full prefetch stale windows

* fix(app-router): retain completed dynamic prefetch bounds
2026-08-10 11:21:29 +01:00
James Anderson d84c4beae3 fix(cache): delegate CDN header cleanup to adapters (#2797)
* fix(cache): delegate CDN header cleanup to adapters

* fix(cache): keep provider headers in CDN adapters

* docs(cache): define adapter header ownership

* test(cache): restore adapter lifecycle coverage

* fix(cache): require Cloudflare-owned CDN adapters

* test(cache): configure adapters in Cloudflare builds

* fix(cache): use the existing Cloudflare CDN adapter

* fix(cache): keep the Cloudflare CDN adapter opt-in

* refactor(cache): keep Cloudflare header policy in CDN adapter
2026-08-10 01:03:14 +01:00
James Anderson 373e8264a1 feat(use-cache): support callable cached functions with rsc plugin api (#2156)
* fix(use-cache): support nested cache functions passed as props

* fix(use-cache): use inline registerServerReference instead of broken forward-reference module-level code

The previous approach used `noExport: true` and appended module-level
`const ${name}_$$vcf` declarations at the end of the transformed file,
then referenced them via forward reference at the call-site. This caused
a temporal dead zone (TDZ) error because `const` bindings are not
hoisted — the call-site assignment evaluated before the TLA const was
initialized, crashing all RSC files that contain function-level "use
cache" (HTTP 500 for use-cache pages, route handlers, etc.).

Fix: keep the existing hoisting/export behaviour (`noExport` stays
false) and instead wrap `registerCachedFunction(...)` with
`registerServerReference(...)` inline at call-site in the RSC
environment. This adds the RSC serialisation metadata ($$typeof, $$id)
so cached functions can be passed as props to client components
(useActionState / formAction), while not disturbing the existing
exported binding that loadServerAction relies on.

* fix(use-cache): use correct normalised id and register in manifest for nested function props

The previous approach passed the raw absolute file path as the $$id to
registerServerReference. @vitejs/plugin-rsc resolves server references by a
normalised key (sha256(toRelativeId) in build; URL-path in dev), so production
would throw "server reference not found" for any cached function passed as a
client-component prop.

Also, the module was never added to the virtual:vite-rsc/server-references
manifest because only the plugin's own "use server" transform writes to
manager.serverReferenceMetaMap. Without a manifest entry, the production
serverReferences lookup has no entry for the module at all.

Fix:
- Capture the plugin-rsc manager via the rsc:minimal plugin API in
  configResolved so we can write to serverReferenceMetaMap directly.
- Compute normalizedRefKey to match vitePluginUseServer's getNormalizedId():
    build → sha256(toRelativeId(id)).hex.slice(0,12)
    dev   → id.slice(root.length)  (Vite URL path)
- After transformHoistInlineDirective succeeds, register the hoisted export
  names in manager.serverReferenceMetaMap[id] so the manifest is populated.
- Pass normalizedRefKey (not raw id) to registerServerReference.

Add unit tests verifying the hash formula matches plugin-rsc's own logic.

* fix(use-cache): wrap hoisted exports as cached server references and register manifest after rsc:use-server

- Derive the build-mode reference key via plugin-rsc's own
  manager.toRelativeId() instead of a string slice, so the hash input is
  byte-for-byte identical to the plugin's hashString(toRelativeId(id)).
- Reassign each hoisted inline 'use cache' export at module level to
  registerServerReference(registerCachedFunction(fn)) so the module
  export itself is the cached wrapper (Next.js parity: direct action
  invocation goes through the cache) and call sites/manifest imports all
  observe the same wrapped function.
- Register serverReferenceMetaMap entries from a new
  vinext:use-cache-server-references plugin placed after the plugin-rsc
  plugins: rsc:use-server deletes metaMap entries for modules without
  'use server', which wiped the entries written during the use-cache
  transform (prod actions 404'd with 'server reference not found').
- Deduplicate the RSC/non-RSC transform branches into a single
  transformHoistInlineDirective call and hoist the
  @vitejs/plugin-rsc/react/rsc resolution out of the per-module path.
- Replace the self-referential key-formula unit test with the ported
  Next.js fixture (use-cache-with-server-function-props/nested-cache), a
  dev-mode Playwright round-trip test, and a production-server
  integration test that resolves the serialized references via action
  POSTs and asserts cached-invoke semantics.

* docs(use-cache): document dev-key normalisation scope for inline cache server references

* fix(use-cache): throw instead of emitting unresolvable inline cache server references when the plugin-rsc manager is missing

When the @vitejs/plugin-rsc manager is unavailable in the rsc environment,
the inline 'use cache' transform previously fell back to a locally computed
reference key and still wrapped the hoisted exports — but the manifest
registration plugin bails without the manager, so the emitted reference
would serialize into the RSC payload yet never resolve (silent 404 on
action POST in production). Fail loudly at transform time instead; the
manager is a structural invariant whenever the rsc environment exists.

Adds transform-level unit tests for the fail-loud path (build + dev), the
non-rsc no-manager control, and build reference-key parity with plugin-rsc.

* test(use-cache): pin unencrypted closure-captured bound args and document the divergence

Extends the nested-fn-props fixture with a cached function that closes over
a value from the cached component's scope, exercising the .bind(null, ...)
bound-arg path end to end: the production round-trip test asserts the
captured value appears in plaintext in the flight payload (pinning the
documented divergence from Next.js, which encrypts bound args by default)
and that invoking the bound reference observes the captured value; the
Playwright test covers the real flight-client encodeReply round-trip in
dev. A transform-level test pins that captures are emitted as plain bind
args. The divergence is now also documented in the README's Known
limitations section.

* refactor(use-cache): route registerServerReference through a vinext shim to decouple from plugin-rsc module-id normalisation

The inline 'use cache' prepend imported registerServerReference from a
file:// URL of @vitejs/plugin-rsc/react/rsc while the cache runtime
imports the same package via the bare specifier, relying on Vite
normalising both to a single module id. Re-export it instead from a new
vinext-owned cache-server-reference shim whose only react/rsc specifier
is the same bare one cache-runtime uses, resolved from the same importer
location — one module instance by construction. The transform unit test
now pins that the emitted import targets the shim and never a plugin-rsc
file URL.

* test(use-cache): pin cached-invoke semantics for the closure-bound getMessage path

Mirror the getDate cache assertion on the closure-bound path: the
fixture's getMessage now appends a Math.random() suffix so cache hits
are observable, and the production-server round-trip asserts that two
identical bound-arg invocations return the same cached value while a
different bound arg misses instead of reusing the entry. The Playwright
assertion matches the suffixed message via regex.

* fix(use-cache): encrypt closure-bound arguments

* refactor(use-cache): use plugin-rsc directive transforms

* test(use-cache): cover directive transforms across environments

* fix(cache): update RSC directive prerelease

* fix(cache): stabilize directive reference tests

* style(cache): format HMR test

* refactor(use-cache): move server function directives to user land

* refactor(use-cache): clarify generic directive plugin naming

* refactor(use-cache): own directive plugin types

* refactor(use-cache): use plugin-rsc metadata map directly

* refactor(use-cache): own server reference metadata lifecycle

* chore(use-cache): keep directive type internal

* refactor(use-cache): adopt server reference claims

* fix(init): install required plugin-rsc prerelease

* feat(rsc): harden use cache server functions

* feat(cache): adopt plugin-rsc transform primitives

* refactor(cache): rename callable plugin

* test(init): update plugin-rsc install expectations

* test(cache): avoid reloading during HMR retries

* test(cache): align callable references with plugin-rsc

* fix(cache): align mixed directives with plugin-rsc 0.5.34

* fix(cache): harden callable use cache transforms

* fix(cache): support manually configured RSC

* fix(cache): harden manual RSC ordering
2026-08-10 00:52:19 +01:00
github-actions[bot] 07cf9e5c58 chore: version packages (beta) (#2726)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 23:15:07 +01:00
Norcleeh a01075ad42 fix(build): throw error if there is _next folder inside the public folder (#2814)
* fix(build): throw error if there is `_next` folder inside the public folder

The `_next` folder under the public folder conflicts
with the internal `_next` route, which is not expected

* fix(build): harden public asset conflict validation

---------

Co-authored-by: James <james@eli.cx>
2026-08-07 21:54:55 +01:00
James Anderson 884259a898 fix(dev): adopt initial Pages stylesheets in Vite (#2825)
* fix(dev): adopt initial Pages stylesheets in Vite

* fix(dev): deduplicate fallback Pages stylesheets

* fix(dev): serve virtual Pages stylesheets
2026-08-07 11:49:12 +01:00
James Anderson 0bc39ef5bd fix(pages): apply fallback rewrites after API misses (#2827)
* fix(pages): apply fallback rewrites after API misses

* fix(pages): distinguish API normalization from rewrites
2026-08-07 11:33:24 +01:00
James Anderson 4210d1bcfd fix(css): resolve Sass tsconfig path aliases (#2824) 2026-08-07 11:11:47 +01:00
Donny/강동윤 86620d5917 feat(client): expose host React to Module Federation remotes (#2828)
* fix(client): bridge React for Module Federation

* fix(ci): register generated React bootstrap entry

---------

Co-authored-by: James <james@eli.cx>
2026-08-06 14:43:11 +00:00
James Anderson 255be4e2ab fix(middleware): align encoded path matching (#2802)
* fix(middleware): align encoded path matching

* test(pages): separate matcher and route identity

* fix(middleware): preserve delimiter matcher parity

* fix(middleware): preserve trailing source delimiters

* test(middleware): align trailing source expectations

* fix(middleware): align trailing slash matcher normalization
2026-08-04 15:20:08 +01:00
James Anderson dfc979cc42 fix(app-router): hand off zero-stale prefetches (#2801) 2026-08-03 22:34:21 +01:00
James Anderson cf689f2e43 fix(pages): run middleware before image endpoint (#2776)
* fix(pages): run middleware before image endpoint

* fix(pages): preserve image rewrite query
2026-08-03 18:33:41 +00:00
James Anderson 2d08de067d fix(pages): preserve raw data URLs for middleware (#2775)
* fix(pages): preserve raw data URLs for middleware

* fix(pages): preserve data route path with skipped normalization
2026-08-03 18:26:09 +00:00
James Anderson 3deb467b21 fix(pages): preserve gSSP headers on redirects (#2771)
* fix(pages): preserve gSSP headers on redirects

* test(pages): cover data redirect headers
2026-08-03 15:43:57 +00:00
James Anderson 81c0b03ef2 fix(pages): preserve not-found response headers (#2773)
* fix(pages): preserve not-found response headers

* fix(pages): preserve not-found headers in dev
2026-08-03 15:59:22 +01:00
James Anderson f9278afafe fix(pages): support bodyless API responses (#2772) 2026-08-03 15:32:50 +01:00
James Anderson ce90ea286a fix(app-router): pass rewrite validation to source middleware (#2798) 2026-08-03 14:08:17 +00:00
Nathan Nguyen 4108dab89c fix(fetch-cache): honor RequestInit in request dedupe (#2762)
* fix(fetch-cache): honor RequestInit in request dedupe

Request inputs with per-call overrides were deduped using the base Request even though fetch executes the overridden headers and options. Distinct authenticated requests could therefore share one response and persist it under the wrong cache key.

Normalize the effective GET or HEAD request before deriving the dedupe key so request-scoped reuse and persistent cache storage stay partitioned by the options the network sees.

* fix(fetch-cache): preserve effective request semantics

* fix(fetch-cache): key inherited request options

* fix(fetch-cache): serialize bodies with effective headers

* fix(fetch-cache): normalize default request options

* fix(fetch-cache): key normalized request body metadata

* fix(fetch-cache): distinguish normalized body variants

* fix(fetch-cache): hash body bytes without loss

* fix(fetch-cache): bound body key fallback work

* test(fetch-cache): lock effective auth bypass

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 15:07:04 +01:00
Nathan Nguyen 7d786e0350 fix(app-router): keep server metadata out of RSC payloads (#2769)
The App Router payload previously serialized partial render observations and slash-prefixed source page paths into document HTML. Server-only cache tags could leak into the client bootstrap, while crawlers could interpret source page metadata as URLs.\n\nKeep complete render observations at cache finalization, transport source pages as validated segments, and retain legacy source strings only for reading cached payloads during rolling deployments. Next-compatible browser source-page diagnostics remain reconstructible from the segment form.\n\nAdd codec, renderer, fallback, and production HTML regression coverage for the wire contract and the absence of server metadata from documents.

Co-authored-by: James <james@eli.cx>
2026-08-03 14:45:02 +01:00
Nathan Nguyen 7bcbe94a25 fix(script): reject event attributes in hoisted HTML (#2763)
App Router hoisting manually serializes beforeInteractive Script props, bypassing React DOM filtering for string-valued event handlers. Request-influenced on* props could therefore become executable inline attributes in the server response.\n\nReject event-handler names case-insensitively at the raw HTML emission boundary while preserving legitimate attributes such as data-onload. The regression test exercises the actual Script capture and hoisted render path.

Co-authored-by: James <james@eli.cx>
2026-08-03 14:26:42 +01:00
Nathan Nguyen 621610afd2 fix(app-router): stop flooring dynamic prefetch stale times (#2757)
* fix(app-router): stop flooring dynamic prefetch stale times

Next keeps its two client stale-time dimensions on separate rules. The
cacheLife/router bound goes through `getStaleTimeMs`, which floors at 30s
(segment-cache/cache.ts). The dynamic bound goes through
`computeDynamicStaleAt` (segment-cache/bfcache.ts), which applies no floor
at all, so `staleTimes.dynamic: 0` means a dynamic payload is never reused
across a navigation.

`resolvePrefetchedRscResponseExpiresAt` floored the *combined* value, which
`resolveRscResponseStaleTimeSeconds` had already min-combined. Since
`serverStaleTimeSeconds` floors the cacheLife half before the min, the outer
floor's only live effect was raising a dynamic bound the resolver's own
comment says it must never raise.

Every dynamic render reports its bound: `app-page-render.ts` defaults
`dynamicStaleTimeSeconds` to `experimental.staleTimes.dynamic` (0), emitted
as a header when the render is known dynamic up front and in the completion
footer when it turns dynamic mid-stream. Flooring that to 30s let a
credentialed RSC payload be replayed for 30s after a logout, role change, or
permission revocation, with no server round-trip. The consumed expiry then
propagated into the visited response cache, extending the window past the
navigation.

Floor only the unsignalled fallback, mirroring `STATIC_STALETIME_MS =
getStaleTimeMs(config)`. A signalled bound is now authoritative.

This also removes the `minimumTtlMs` plumbing, a partial workaround that
zeroed the floor for routes with a dynamic *pattern* segment. It keyed on
the wrong axis — a statically-patterned route rendering dynamically
(`/dashboard` reading `cookies()`) never matched — and the correct fix
subsumes it. The one test that relied on it now drives the same assertion
through the header a real dynamic render sends.

* fix(app-router): scope the dynamic bound to automatic prefetches

CI caught that the previous commit applied the dynamic stale-time bound to
`prefetch={true}` as well, breaking segment-cache-metadata's rewrite reuse.

Next splits reuse by prefetch kind, not only by stale-time dimension. In
`getPrefetchEntryCacheStatus` a `full` prefetch stays reusable up to
STATIC_STALETIME_MS even for dynamic content; only `auto` degrades past
DYNAMIC_STALETIME_MS. The segment-cache/bfcache.ts comment states it
directly — dynamic prefetches "use STATIC_STALETIME_MS instead of
DYNAMIC_STALETIME_MS" — and the upstream metadata test says so in prose:
"Because the link is prefetched with prefetch={true}, we should be able to
prefetch the title, even though it's dynamic."

So `prefetch={true}` is an explicit opt-in to holding dynamic content for the
static window. Carry that on the policy as `honorDynamicStaleTime`: true for
`resolveAutoAppRoutePrefetch`, false for `resolveFullAppRoutePrefetch`. A
full prefetch resolves its expiry from cacheLife alone, still floored at 30s
per `getStaleTimeMs`; an automatic one additionally honors the dynamic bound.

This is the axis the removed `minimumTtlMs` was groping for — it keyed on
route-pattern dynamism, which is neither the prefetch kind nor the render
kind.

* fix(app-router): keep the prefetch floor for explicit full prefetches

CI showed the previous commit went too far the other way: dropping the
dynamic bound entirely for `prefetch={true}` stretched those windows from 30s
to the full 300s static TTL, and client-cache's parallel-route reuse tests
started re-issuing full prefetches.

Scope the change to the floor rather than to which bounds apply. An automatic
prefetch takes a dynamic render's bound verbatim — including below the 30s
prefetch floor — so a `0` expires immediately, which is the vulnerability.
`prefetch={true}` still min-combines both bounds but keeps Next's ≥30s
prefetch floor, reproducing the previously-green behavior for full prefetches
exactly.

That confines the behavioral change to automatic prefetches, which is where
the finding lives: default `<Link>` prefetching of a dynamically rendered
route.

* ci: retrigger client-cache e2e (suspected flake)

* chore: drop e2e fixture node_modules symlinks committed by mistake

* chore: retrigger CI

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 12:27:09 +01:00
Nathan Nguyen 5f63ae4ae1 fix(app-router): validate external RSC rewrites before proxying (#2754)
* fix(app-router): validate external RSC rewrites before proxying

Out-of-basePath RSC requests claimed by basePath:false rewrites could reach external destinations before their missing or stale _rsc token was canonicalized. This bypassed cache-busting validation in every rewrite phase.

Require every external rewrite call to validate a claimed request before proxy I/O. Regression coverage exercises GET and HEAD in beforeFiles, afterFiles, and fallback and verifies the upstream is never contacted for invalid tokens.

* fix(app-router): validate middleware external RSC rewrites

* fix(app-router): preserve external RSC proxy state

* fix(app-router): preserve canonical RSC redirects

* fix(app-router): restore external Flight headers

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 12:12:42 +01:00
Nathan Nguyen aec4421b2a fix(app-router): let concrete Pages routes win middleware rewrites (#2730)
* fix(app-router): let concrete Pages routes win middleware rewrites

A Pages data request that middleware rewrote returned a synthetic empty
JSON body whenever any App route matched the rewrite target, including a
dynamic or catch-all match. Every other App-vs-Pages ownership decision in
this handler treats a dynamic App match as non-owning, so a concrete Pages
route at the same pathname should render instead. Skipping that arbitration
meant getServerSideProps never ran for the rewrite target, and the client
router, which reuses the middleware probe response when the rewrite target
resolves to a Pages route, accepted the empty body as successful page data.
Redirect and notFound markers the Pages route would have returned were
therefore absent during client-side navigation.

Restrict the shortcut to App matches that own the target outright, so
dynamic matches fall through to the existing static and dynamic Pages
fallback arbitration.

That fallthrough reaches the tail Pages data response, which built its
headers from the not-found response alone and dropped headers the
middleware set on the way. Merge the middleware response headers there so
a rewrite landing on a genuinely App-owned dynamic route still carries its
cookies.

* fix(app-router): preserve middleware headers on Pages fallbacks

* fix(app-router): use Pages response merge semantics

* test(app-router): cover rewritten Pages data ownership

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 11:02:14 +01:00
Nathan Nguyen 9370ea6937 fix(app-router): keep mounted-slot RSC responses no-store (#2728)
* fix(app-router): keep mounted-slot RSC MISS responses no-store

finalizeAppPageRscCacheResponse derived "should I rewrite the client
headers?" from the return value of scheduleAppPageRscCacheWrite. Those are
independent decisions, and #2497 made them disagree: mounted-slot variants
now correctly skip the persistent write (their RSC key is slot-blind), but
the early return took the pending-dynamic finalization with it.

The result is that a fresh ISR-eligible RSC MISS carrying
X-Vinext-Mounted-Slots leaves the origin with its initial
`s-maxage=..., stale-while-revalidate` instead of being rewritten to
`no-store, must-revalidate`. That header is what stops a shared cache from
storing a stream that may still reach cookies()/headers() below a Suspense
boundary after the cache policy was chosen, so a personalized payload can be
stored and replayed for the URL/variant. Apps with named parallel routes send
the header on essentially every client navigation; apps without slots never
enter the path.

Gate the header rewrite on preserveClientResponseHeaders alone, which is
already `cacheState !== "MISS"` at the only production call site. This
restores the client-facing behavior that shipped before #2497 while keeping
its cache-write change, and matches finalizeAppPageHtmlCacheResponse, which
never coupled the two. Doing it structurally rather than adding a
mountedSlotsHeader term means the next early return added to
scheduleAppPageRscCacheWrite cannot silently reintroduce this.

* fix(app-router): keep mounted slots out of edge caches

* fix(cache): clear mounted-slot CDN overrides

* docs(cache): explain mounted-slot no-store scope

* fix(cache): clear pending CDN overrides

* docs(cache): clarify pending header policy

* test(cache): cover dynamic mounted-slot headers

* test(cache): cover pending HTML CDN overrides

---------

Co-authored-by: James <james@eli.cx>
2026-08-03 10:58:41 +01:00