Commit Graph

1925 Commits

Author SHA1 Message Date
James Anderson fd2a8bb66a feat(cache): export stable io from next/cache, deprecate unstable_io (#1142)
* feat(cache): export stable `io` from next/cache, deprecate `unstable_io`

Next.js stabilized `unstable_io` as `io` in vercel/next.js#93621 without a
deprecation alias. vinext shipped the unstable name longer than upstream
canaries (since #805), so keep `unstable_io` as a transitional alias that
emits a one-time deprecation warning.

Closes #1139

* chore: apply formatter
2026-05-08 21:31:52 +01:00
Nathan Nguyen ba21f50d7c fix(app-router): emit action revalidation headers (#1117)
* fix(app-router): emit action revalidation headers

Server Actions that call revalidatePath, revalidateTag, updateTag, refresh, or mutate cookies currently return RSC responses without x-action-revalidated. The client router therefore keeps stale cached payloads after an action invalidates data.

The action execution path assumed cache invalidation only needed to touch the server-side cache handler. Record action-scoped revalidation in the cache shim, consume it when building action responses, and preserve Next.js' static-and-dynamic versus dynamic-only header values.

Tests cover normal action rerenders, redirecting actions, and refresh dynamic-only invalidation.

* fix(app-router): match profiled tag action revalidation

Server Actions using revalidateTag with a cacheLife profile should not trigger x-action-revalidated. Next.js treats profiled tag revalidation as stale-while-revalidate and does not force immediate client router cache invalidation.

Only record action revalidation for the no-profile revalidateTag form, keep the action marker cleanup symmetric on error, and cover the profiled tag case in server action tests.

* fix(app-router): notify actions for immediate tag expiry

Server Actions that call revalidateTag with an inline profile of expire zero immediately expire server cache data, but the client router still needs x-action-revalidated to drop stale RSC payloads.

Resolve revalidateTag profiles before deciding whether to mark action revalidation, matching Next.js' no-profile or expire-zero condition. Also clear the action marker on progressive action errors for symmetry with the RSC action path.

* fix(app-router): notify on revalidateTag with unknown profile

Address review feedback:
- revalidateTag with an unrecognized profile string resolves to no
  durations and the handler treats it as a full invalidation, but the
  client router was not being notified. Mark action revalidation when
  durations is missing too.
- Document the cookie override path in resolveActionRevalidationKind so
  future readers see why it bypasses the max-escalation pattern used in
  markActionRevalidation.

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 20:30:13 +00:00
Nathan Nguyen ae151bb220 fix(app): preserve middleware headers on cached pages (#1127)
* fix(app): preserve middleware headers on cached pages

* fix(app): preserve middleware status on cached pages
2026-05-08 21:25:34 +01:00
Nathan Nguyen 2068b8c343 perf(prerender): reuse embedded RSC payload (#1097)
* perf(prerender): reuse embedded RSC payload

* fix(prerender): address RSC extraction review

* fix(test): include artifact compatibility metadata

* fix(prerender): document RSC extraction invariants

* refactor(prerender): simplify RSC extractor and remove dead fallback

Cleanups on top of #1097:

1. Drop the legacy `__VINEXT_RSC__` extractor. The browser entry still
   recognizes that format for in-the-wild cached HTML, but prerender
   parses HTML it just emitted in this build, where the writer always
   emits the chunked form. The legacy branch is unreachable here.

2. Remove the silent fallback to a second `RSC: 1` invocation. The
   writer is in-tree, so a missing-marker case is a vinext-internal
   regression we want to surface loudly rather than mask by
   double-rendering. `extractRscPayloadFromPrerenderedHtml` now
   returns `string` and throws on any anomaly.

3. Replace the manual JSON-string tokenizer (parseEmbeddedJsonString +
   assertOnlyTrailingSemicolon + isWhitespace, ~80 lines) with a
   ~12-line slice + JSON.parse. Relies on the same safeJsonStringify
   invariant the original parser relied on; tampering with trailing
   code now surfaces as 'invalid chunk JSON' instead of a separate
   diagnostic, which is fine for an internal protocol.

4. Remove the obsolete TODO marker ("Extract RSC payload from the
   first response instead of invoking the handler twice") — the PR
   resolves it.

7. Tighten the chunk-presence assertion: empty HTML or HTML with only
   the done marker now throw 'no chunk scripts found in HTML' instead
   of returning the fallback sentinel.

Test updates mirror these:
- Drop the two legacy-format tests.
- Drop the "falls back to a separate RSC request" integration test
  (the fallback no longer exists).
- Update error-message assertions where the diagnostic text changed.

* fix(prerender): fall back to second invocation for non-render HTML responses

The previous cleanup made `extractRscPayloadFromPrerenderedHtml` throw
on any HTML lacking RSC embed chunks. That's wrong for one legitimate
case: middleware that short-circuits the App Router pipeline with a
custom 200 HTML response — that HTML never went through
createRscEmbedTransform, so it has no chunks and no done marker.

Distinguish two cases at the extractor level:
- No chunks AND no done marker → middleware short-circuit. Return null
  so the caller falls back to a second invocation with `RSC: 1`.
- Done marker present without chunks → real bug, throw.
- Chunks present without done marker → real bug, throw.
- Malformed/tampered chunk JSON → real bug, throw.

The driver checks for null and recovers by issuing a second handler
invocation with RSC headers, writing whatever that returns as the
.rsc file.

Adds an integration test alongside the existing dedup test:
- Middleware short-circuit: server returns 200 HTML without chunks for
  page requests, valid Flight for RSC: 1 → driver writes the fallback
  payload as .rsc, with exactly one of each request kind.

* fix(prerender): fail non-ok RSC fallback responses

---------

Co-authored-by: James <james@eli.cx>
2026-05-08 21:23:28 +01:00
Morty 7e77fb5d25 fix: add shell: true on Windows for execFileSync calls (#742)
* fix: add `shell: true` on Windows for execFileSync calls

On Windows, `execFileSync` cannot execute `.bin/wrangler` (a shell
script) or package manager commands like `npm`/`npx` (which are `.cmd`
wrappers) without enabling the shell. This causes `ENOENT` errors
during `vinext deploy`, `vinext build`, and `vinext lint`.

Fix by adding `shell: process.platform === "win32"` to all
`execFileSync` options. This only enables the shell on Windows where
it's required, preserving the no-shell-injection guarantee on other
platforms.

Fixes `spawnSync ENOENT` on Windows for:
- `runWranglerDeploy` (deploy.ts)
- `installDeps` (deploy.ts)
- React upgrade in `deploy()` (deploy.ts)
- React upgrade in `buildApp()` (cli.ts)
- Lint commands in `lint()` (cli.ts)

* style: break long execFileSync lines to fit printWidth: 100

* fix: use ExecFileSyncOptions for execFileSync opts type

ExecSyncOptions.shell is typed as `string | undefined`, while
ExecFileSyncOptions.shell is `boolean | string | undefined`. The
explicit annotation rejected the new `shell: process.platform === "win32"`
boolean. Switching to the matching options type resolves the TS2322
error reported by `vp check` in CI.

* docs: update execFileSync comment for cross-platform shell behavior

Reflects shell: true on win32 — the array form still prevents trivial
injection, but the older comment implied shell-free execution everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:21:27 +01:00
Nathan Nguyen 37ca6be63b fix(app-router): isolate middleware request bodies (#1132)
Middleware could consume the same ReadableStream that downstream App Router handling later needed to read. This broke POST route handlers when middleware inspected request.json() and then returned NextResponse.next().

The root cause was constructing middleware NextRequest instances from the caller-owned Request body. Clone the request body branch before wrapping or pathname-normalizing middleware requests, and cover both the constructor and middleware pipeline behavior with regression tests.
2026-05-08 21:18:10 +01:00
James Anderson 0f05e53f1f bump setup-vp (#1135) 2026-05-07 14:35:08 +00:00
Nathan Nguyen 937f5d6a09 fix(app-router): read indefinite app page cache entries (#1129)
* fix(app-router): read indefinite app page cache entries

App pages with revalidate=false currently normalize to Infinity, but the app-page cache read gate excludes Infinity. Pre-rendered static app pages can be seeded into the ISR cache and still miss that cache on every production request.

The cache read policy now treats positive Infinity as read-eligible while preserving force-dynamic, production, RSC, and nonce guards. Cached indefinite responses are normalized to the same static one-year Cache-Control header used by fresh revalidate=false responses, so cache hits do not emit s-maxage=Infinity.

Tests cover the dispatch cache hit path and the shared cached Cache-Control helper.

* chore: rerun ci

* chore: rerun ci
2026-05-07 11:23:28 +00:00
James Anderson ea096701da ci: pin pnpm in cna-test to vinext's package.json version (#1131)
The create-next-app job was failing with ERR_PNPM_UNEXPECTED_STORE.
create-next-app's bundled pnpm (10.x) installs node_modules linked to
the v10 store; vp's bundled pnpm recently bumped to v11, which then
refuses to add packages to the v10-linked node_modules.

Anchor the scaffolded project to vinext's own pinned pnpm version
(read from root package.json's packageManager field) and invoke that
pnpm directly via npx for the install step. This:

- keeps store layouts aligned between create-next-app's install and
  the subsequent vinext add
- stays in sync with future pnpm bumps without further CI edits
- bypasses vp's bundled pnpm only for this layering step; the rest of
  the job (vp exec, vp dlx) is unchanged
2026-05-07 11:14:19 +00:00
Nathan Nguyen 04368ebbcb fix(middleware): preserve credentials for external override rewrites (#1121)
Middleware request-header overrides could rebuild forwarded external rewrite requests from only the override set. That drops cookie and authorization when middleware reports a partial override, so proxied upstream requests lose credentials.

Keep Next.js-compatible deletion semantics by default, and make credential preservation an explicit opt-in for external proxy boundaries. Cover both partial preservation and explicit credential deletion.
2026-05-07 11:24:04 +01:00
Nathan Nguyen 38e6b9f6c2 fix(app-router): bypass app page cache in draft mode (#1123)
App Page ISR could serve a cached production response to a request carrying a valid __prerender_bypass cookie. That violates Next.js draft-mode semantics because preview users must render dynamically and must not populate shared page cache entries.

The dispatch layer now identifies draft-mode requests with the same cookie parser and secret used by draftMode(), skips App Page cache reads, and passes that state into render response policy. HTML and RSC render paths now treat draft mode as no-store and avoid cache capture/write work.

Tests cover valid draft cookies bypassing cached HTML, arbitrary cookie values preserving cache hits, and response-policy no-store behavior.
2026-05-07 11:23:33 +01:00
Nathan Nguyen a4497f2bf8 fix(actions): return HTTP fallback status for RSC action notFound (#1122)
RSC fetch actions that threw notFound() or another HTTP access fallback still returned a Flight payload with status 200. That diverged from Next.js, which sets the response status to the access fallback status before generating Flight for fetch actions.

The server action path now extracts the fallback status while preserving the action error in the Flight return value. The regression test covers legacy NEXT_NOT_FOUND, modern NEXT_HTTP_ERROR_FALLBACK;404, and another fallback status.
2026-05-07 11:23:03 +01:00
Nathan Nguyen 7b3fc49b92 fix(app-router): honor route group error boundaries (#1124) 2026-05-07 11:22:30 +01:00
Nathan Nguyen 4932b69b17 feat(router): expose RouteManifest semantic facts (#1113)
* feat(router): expose RouteManifest semantic facts

RouteManifest only exposed minimal route, layout, template, and slot lookup maps. That left later #726 planner and cache-proof work without stable graph-owned facts for slot ownership, slot defaults, template reset scope, or boundary outcomes.

The graph builder now derives declaration-level slot facts, per-route slot bindings, template remount facts, and layout-boundary outcome facts from existing App Router metadata. The manifest graphVersion includes the new maps so downstream compatibility checks can detect semantic topology changes.

Tests cover route groups, slots, defaults, templates, and boundaries in one fixture.

* test(router): align CI fixtures with graph facts

Full CI checks more App Router fixtures than the targeted RouteManifest test run. The new ParallelSlot declaration fields must be present in hand-built fixtures, otherwise typecheck fails even though production graph scanning populates them.

The app browser trace tests also still asserted the stale reason at entry zero, but no-commit transaction tracing is prepended before the reason entry. Update those assertions to inspect the stale reason entry without changing runtime behavior.
2026-05-07 09:15:43 +01:00
Nathan Nguyen 91b18396d0 fix(app-router): prevent repeated hard-navigation loops (#1111)
* fix(app-router): guard repeated hard navigation targets

Root-boundary changes currently trigger a hard navigation without remembering the target. If an old client keeps receiving an incompatible new-server payload for the same URL, the recovery path can re-enter the same hard navigation instead of stopping the loop.

Persist the attempted hard-navigation target in sessionStorage, block a repeated same-target attempt once the browser is already on that URL, and clear the guard after a successful initial RSC bootstrap. Add old-client/new-server future compatibility coverage so unknown metadata remains a render-fresh fallback, not reuse proof.

* test(app-router): cover cross-page hard navigation guard

Bonk noted that the hard-navigation guard allows cross-URL recovery even when sessionStorage cannot persist the guard, and that same-URL server-action hard navigation intentionally ignores the guard result.

Document those invariants in the controller and add coverage proving a stored guard target does not block a hard navigation when the browser is currently on a different URL.
2026-05-07 09:13:07 +01:00
Nathan Nguyen 5ee9860b24 fix(middleware): preserve NextResponse.next status (#1115)
NextResponse.next({ status }) currently continues routing but drops the middleware response status. That diverges from Next.js middleware handling and makes successful route rendering mask middleware-selected statuses such as 404.

The middleware runtime now exposes a status override for continue responses, and App Router plus Pages Router dev/prod consumers feed that value into their existing middleware response contexts. Rewrite status behavior remains covered by the same path.

Tests cover the middleware runtime contract, App Router context propagation, and a Pages Router dev integration case with a real matched page.
2026-05-07 09:01:04 +01:00
Steve Faulkner 0e62dc24c7 Prefer Pages Router route params over same-key query params (#1119) v0.0.49 2026-05-06 15:01:39 -05:00
Steve Faulkner 37ea7a9442 Align React RSC init floor with 19.2.6 (#1118) 2026-05-06 12:54:04 -05:00
Nathan Nguyen ca2ee087ac fix(app-router): keep ISR app cache regeneration variant-safe (#1116)
Stale App Router RSC cache entries currently schedule background regeneration with only the clean pathname. That violates the cache-key invariant because RSC app-page entries include the mounted slot variant, so concurrent stale variants for one pathname can collapse into one pending regeneration.

HTML ISR cache writes also read late dynamic usage from the cache branch after the client branch can clear request context. When dynamic APIs are discovered while streaming, the cache writer can miss that signal and persist personalized output.

Use the actual app-page ISR cache key as the regeneration dedup key, and snapshot late dynamic usage immediately before request context cleanup so the HTML cache writer can still skip writes after stream teeing. Add focused regression coverage for slot-specific stale regeneration and pre-cleanup dynamic usage.
v0.0.48
2026-05-06 18:37:44 +01:00
James Anderson b631b5da4a chore(deps): bump next.js version (#1112)
* chore(deps): bump next.js version

* fix tests
2026-05-06 17:32:01 +00:00
Jim Geurts 49a4376508 fix(config): handle ssr.external: true in App Router (closes #1103) (#1105)
* fix(config): exclude React from ssr optimizeDeps when ssr.external: true

When users set `ssr.external: true` in vite.config.ts (the only practical
way to keep CJS-quirky packages like pg, protobufjs, keyv,
@launchdarkly/js-server-sdk-common, @opentelemetry/otlp-transformer
working in dev), `'use client'` components calling React.useContext from
external packages (nuqs, next-themes, launchdarkly-react, etc.) crashed
in dev with `Cannot read properties of null (reading 'useContext')`.

Root cause: @vitejs/plugin-rsc populates
environments.ssr.optimizeDeps.include with React entries via
crawlFrameworkPkgs. With external: true, the SSR env loads React via
Node's resolver, but Vite still pre-bundles a duplicate React copy into
deps_ssr/ because optimizeDeps.include overrides external: true.
react-dom-server.edge then sets the dispatcher on its bundled React
while externalized callers (vinext's runtime) see a different React,
leaving React.H null and crashing every hook call from a 'use client'
module SSR'd through it.

Fix: when userSsrExternal === true, add React entries to
environments.ssr.optimizeDeps.exclude so plugin-rsc's auto-includes
cannot pre-bundle a duplicate copy. The renderer and the runtime then
share one Node-loaded React.

The RSC env is unaffected (it still pre-bundles React with the
react-server condition), and the default mode (`noExternal: true`) is
unaffected since external: true gating the strip.

Closes #1103

* fix(config): also skip top-level ssr.noExternal: true when ssr.external: true

The previous fix only stripped React entries from
environments.ssr.optimizeDeps.exclude, but Vite uses top-level `ssr.*`
as defaults for `environments.ssr.*`. With vinext setting top-level
`ssr.noExternal: true` unconditionally (when not Cloudflare/Nitro),
that `true` leaked into `environments.ssr.resolve.noExternal` and
@vitejs/plugin-rsc's later expansion to an array (or the user's own
configResolved mirror) brought React back into the bundled list,
recreating the duplicate-React crash.

Two changes:

1. When `config.ssr.external === true`, vinext only sets the top-level
   `ssr: { external: true }` block — no `noExternal: true` to leak into
   the per-env config.

2. Defensive configResolved hook that strips React entries from
   `environments.ssr.resolve.noExternal` if a later plugin (or
   plugin-rsc itself) added them back. The strip is gated on
   `external === true` so it only applies in the all-external mode.

Verified against a real-world App Router app (~80 deps including nuqs,
next-themes, launchdarkly-react, blocknote, opentelemetry, pg,
protobufjs, keyv, etc.): with ssr.external: true and these fixes
applied, 'use client' routes now render correctly in dev — the
`Cannot read properties of null (reading 'useContext')` /
`Invalid hook call` crashes are gone.

Refs #1103
2026-05-06 18:29:53 +01:00
Nathan Nguyen 23ab1793f6 fix(app-router): strip internal headers around middleware (#1109)
App Router middleware currently receives internal Flight request headers, and external middleware rewrite proxy responses can return upstream x-middleware-* control headers when no middleware header context exists. That exposes internal protocol state at request and response boundaries.

The middleware entry now clones requests with the Next.js Flight header set removed before middleware execution or middleware-owned external rewrites. External middleware rewrite proxy responses are always sanitized with the existing middleware header scrubber before any middleware headers are merged.

Regression coverage asserts middleware cannot observe Flight headers and that upstream proxy x-middleware-* headers are stripped without context.
2026-05-06 18:12:33 +01:00
Nathan Nguyen 2009afde51 feat(app-router): trace approved visible commit outcomes (#1101)
Visible router state currently has an approved commit path, but the raw reducer remains an exported mutation escape hatch and approval traces only explain low-level reasons. That makes it harder to audit whether a payload became visible, was discarded, or forced a hard navigation.

The violated invariant is that visibleCommitVersion should have one owner behind ApprovedVisibleCommit, with every approval decision carrying an operator-visible transaction outcome.

Move the reducer implementation behind applyApprovedVisibleCommit, keep visibleCommitVersion advancement in that boundary, and prepend transaction trace codes for visible commits, no-commit decisions, and hard navigations.
2026-05-06 17:50:34 +01:00
Nathan Nguyen 9469e7798e fix(router): gate refresh and traverse outcomes (#1102)
* fix(router): gate refresh and traverse outcomes

Implements #726-CORE-05/06A and #726-CORE-06B from issue #726.

Compare pending operation visible commit versions against the latest visible state before applying refresh, traverse, or same-URL navigation payloads. Return explicit navigation payload outcomes so stale and hard-navigation results settle without seeding visited-response cache entries.

Make router.prefetch entries expose a cache-seeded outcome only after the snapshot is ready and pending work is cleared, and keep the ambient next/navigation declarations aligned with the runtime cache shape.

* chore: retrigger ci
2026-05-06 17:47:08 +01:00
James Anderson c74faed38c chore(deps): bump react version (#1110) 2026-05-06 16:46:47 +00:00
Nathan Nguyen ebbf72cc8d feat(router): expose route graph manifest read model (#1089)
* feat(router): expose route graph manifest read model

The App Router graph now exposes root-boundary identity and a stable RouteManifest read model instead of leaving these facts implicit inside per-route arrays. Future #726 planner and compatibility work needs a semantic lookup surface for routes, layouts, pages, templates, slots, and root boundaries before it can stop reading meaning from transport keys.

This adds RootBoundaryId, graphVersion, StaticSegmentGraph maps, and fail-fast invariant checks for mismatched layout/template positions while preserving the existing routes array for current callers.

* chore: retrigger ci
2026-05-06 17:44:20 +01:00
Nathan Nguyen faa5c2feca fix(app-router): propagate generateStaticParams validation errors (#1108)
Request-time dynamicParams=false validation swallowed thrown and rejected generateStaticParams failures. That made application errors look like either successful validation or a 404 when a later source excluded the params.

The validation helper now lets generateStaticParams failures reject, matching the Next.js static params builder contract. The dispatch caller no longer passes a logging callback whose only recovery path was to continue after the failure.

Regression tests cover thrown, rejected, and partially failing generateStaticParams sources.
2026-05-06 17:37:55 +01:00
Nathan Nguyen 09059e7dec fix(actions): cap decoded server action arguments (#1107)
Fetch server actions accepted every value returned by decodeReply and spread that array into action.apply. A hostile action payload could force an oversized argument list and hit the same stack-overflow class Next.js guards against.

Add the Next.js-compatible 1000 argument limit immediately before invoking the action, preserving the existing action error packaging path. Cover the regression with a focused fetch-action test that proves the action is not called for 1001 decoded arguments.
2026-05-06 17:37:29 +01:00
Nathan Nguyen ccc2d7b640 fix(router): canonicalize App RSC route-match path parts (#1106)
App RSC route matching trusted callers to pass already-normalized pathname parts. That let encoded static segments miss and caused interception source params to be dropped when source paths still contained URL-encoded parts.

The matcher now reuses the existing route pathname normalizer before splitting both direct and interception source paths. Regression coverage verifies encoded static segments, exactly-once dynamic param decoding, and merged interception params.
2026-05-06 17:37:00 +01:00
Nathan Nguyen ceb846f5b0 fix(router): reject stale same-url server action commits (#1100)
* fix(router): reject stale same-url server action commits

Same-URL server action payloads could commit visible router state after newer visible work had already won. URL equality and activeNavigationId alone did not distinguish two different visible worlds at the same href.

The lifecycle gate now compares a pending operation's started visibleCommitVersion against the current router state's visibleCommitVersion at approval time. Stale results keep returning their action value, but they no longer mutate visible state or trigger stale hard-navigation recovery.

Tests cover out-of-order same-URL server actions and the visible-version stale path taking priority over root-boundary hard navigation.

* fix(router): preserve server action initiation version

Server action POST handling captured the router state before fetch but the same-URL commit helper previously rebased the pending operation when the response was decoded. If a newer visible commit landed while the POST was in flight, the old action could still look current.

Thread the initiation state through same-URL action commit handling so startedVisibleCommitVersion reflects the world that submitted the action. Approval still compares against the live router state after payload decoding, so stale results keep their action value without mutating visible state.

Add a hostile race test where the action response is processed after a newer same-URL visible commit.
2026-05-06 13:06:44 +01:00
Nathan Nguyen 5cf508d72d feat(prerender): add concurrency flag (#1096) 2026-05-06 10:12:28 +01:00
Christoph Richter 3cfccd0bd4 fix(deploy): resolve wrangler .CMD shim on Windows (#1098)
* fix(deploy): resolve wrangler .CMD shim on Windows (#1095)

`runWranglerDeploy` invoked the bare-name `node_modules/.bin/wrangler`
file, which on Windows is a Unix shebang script that CreateProcess()
cannot execute, surfacing as a misleading `spawnSync wrangler ENOENT`.
Prefer the `.CMD` shim on win32, fall back to the bare name on other
platforms (and as an error-message fallback when nothing is found).

Extracted bin resolution into an exported, platform-injected
`resolveWranglerBin` helper so the behavior is unit-testable without
mutating `process.platform`.

Fixes #1095

* Apply suggestion from @ask-bonk[bot]

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-06 09:11:23 +00:00
Nathan Nguyen 475a7d8286 feat(server): add disabled cache proof model (#1093)
* feat(server): add disabled cache proof model

Route-scoped cache work currently lacks a typed proof boundary for representing render observations, variants, and breaker fallback decisions. That makes future cache reuse work easy to wire before proof completeness is explicit.

The missing invariant is that cache proof data must be canonical, redacted, bounded, and incapable of authorizing runtime reuse while the model is disabled.

Add a v0 cache proof model with RenderObservation, CacheVariant, BoundaryOutcomeCompatibility, variant budgets, and breaker fallbacks. The disabled decision always returns canReuse false, and tests cover canonical redacted dimensions, budget breaker paths, negative request API proof completeness, exact boundary outcome matching, and the disabled reuse gate.

Refs #726

* fix(server): use distinct invalid cache budget code

Invalid cache proof budget fields currently report the route variant ceiling reason code, even when the failing field is unrelated to route cardinality.

That conflates malformed configuration with an actual per-route variant ceiling breach.

Add CP_INVALID_VARIANT_BUDGET for invalid budget fields while leaving CP_ROUTE_VARIANT_CEILING_EXCEEDED for real route variant count failures. Cover maxEncodedLength as the regression case.

Refs #726

* fix(server): harden disabled cache proof invariants

Disabled cache proof variants currently collapse null and empty-string output scope fields into the same encoded value. That makes a future empty-string field addition able to collide silently with absent values.

The matcher also leaves two defensive contracts implicit: breaker fallback detection depends on the generic kind field, and the private boundary matcher would treat unknown as a match if called directly.

Preserve null through JSON output encoding, use the fallback-specific code field for the internal breaker guard, and make unknown outcomes non-matching at the matcher level. Add coverage for null versus empty-string scope keys and for missing request API observations not counting as negative proof.

Refs #726
2026-05-06 10:05:06 +01:00
Nathan Nguyen 70410ba723 feat(prerender): log server startup (#1094) 2026-05-06 09:28:53 +01:00
Nathan Nguyen f1c27bbf0c fix(prerender): render layout-only parallel slot routes (#1091)
Layout-only App Router routes with parallel slot content were omitted from prerender because the collection loop treated route.pagePath as the only renderable UI entry. That silently skipped static export output for routes whose visible content comes from slot page or default modules.

Use a shared App Router render-entry helper for prerender and build-report classification. It keeps route handlers API-only, preserves normal children page priority, and falls back to parallel slot page/default modules for layout-only UI routes.

Covers the regression with prerender output assertions for /parallel-nested/home, /parallel-nested/home/nested, and /slot-collision, plus build-report classification coverage.
2026-05-06 08:25:47 +01:00
Nathan Nguyen 41d62e984e refactor(app-router): gate browser state writes with approved visible commits (#1090)
* refactor(app-router): gate browser state writes with approved visible commits

Browser router commits currently mutate visible state from raw pending navigation actions after separate disposition checks. That leaves lifecycle approval implicit at each writer, which makes later stale-operation and planner work easier to bypass by accident.

Introduce CommitDecision and ApprovedVisibleCommit as the proof object for visible browser commits. Route navigation, same-URL action, and HMR state writes through the approved commit path while preserving the legacy dispatch, skip, and hard-navigate dispositions for current callers.

Tests cover approved navigate, replace, traverse, stale no-commit, and hard-navigation decisions, alongside the existing browser lifecycle regression coverage.

* refactor(app-router): close visible commit approval escape hatch

* ci: rerun checks

* refactor(app-router): isolate visible commit approval authority

* refactor(app-router): remove legacy commit disposition mirror

Visible commit classification still exposed a translated legacy disposition beside the authoritative decision. That left same-URL server action handling able to read two equivalent fields and made the approval boundary less clear than it needs to be.

Use the commit decision directly, remove the translation helper, keep the old state-level disposition private, and assert the non-visible trace reason codes in the approval tests.
2026-05-06 08:24:30 +01:00
Nathan Nguyen b94650bcfb refactor(app-router): fence AppElements wire-key construction (#1088)
* refactor(app-router): fence AppElements wire-key construction

AppElements transport keys could still be constructed or recognized through scattered raw string prefixes outside the wire codec. That kept the flat payload format acting as an informal semantic API and left legacy compatibility behavior covered only by indirect route tests.

Make AppElementsWire the exported constructor and parser boundary for route, page, layout, template, slot, cache, metadata, and unmatched-slot wire values. Route graph semantic ID minting stays separate, while render wiring, layout classification, browser slot merging, and prefetch cache tests now consume the codec boundary.

Tests cover canonical key construction and parsing, legacy metadata read/write behavior, unmatched-slot marker compatibility, and a source-boundary check that rejects new raw AppElements wire-key construction outside the codec and route graph.

* refactor(app-router): address AppElements wire review

Make slot-key recognition a cheap structural predicate, document the absolute tree-path parser contract, and broaden the boundary coverage to catch raw wire-key concatenation.

Also route layout classification ids through AppElementsWire so raw layout-key construction stays fenced inside the codec boundary.
2026-05-06 08:23:01 +01:00
Nathan Nguyen 15c8079604 feat(app-router): add artifact compatibility metadata (#1062)
* feat(app-router): add artifact compatibility envelope

App Router payloads now carry a parsed compatibility envelope at the existing payload metadata boundary. Without this skeleton, future cache and skip work would have no typed place to record graph, deployment, schema, root-boundary, or render-epoch proof.

Add a small constructor/parser pair, attach a default unknown-proof envelope to outgoing record payloads, and reject malformed envelope metadata when reading payload metadata. Tests cover explicit envelopes, legacy missing envelopes, malformed envelopes, and non-mutating payload writes.

* test(app-router): cover future artifact schema rejection

The review correctly identified that the envelope parser had malformed-field coverage but no direct test for the version guard path. Add the future-schema-version case and document why the Wave01 skeleton emits separate schema versions and an unknown-proof envelope without yet splitting mismatch from corruption behavior.

* feat(app-router): attach artifact compatibility metadata

App Router payloads previously carried only the COMPAT-01 unknown envelope unless a caller manually supplied metadata. That left COMPAT-02/03 without concrete graph, deployment, root-boundary, and fallback behavior in the PR.

The render lifecycle now attaches compatibility metadata for record payloads from route/root/deployment facts, while renderEpoch remains unknown until an epoch owner exists. The compatibility evaluator returns explicit compatible, unknown, or incompatible decisions so unknown proof always falls back to render fresh instead of becoming reuse proof.

Tests cover render-boundary metadata attachment and unknown graph, deployment, root-boundary, and render-epoch decisions.

* fix(app-router): tolerate unused compatibility metadata parse failures

App Router metadata parsing crashed when __artifactCompatibility was malformed or from a future schema, even though current render and navigation callers do not consume that field yet.

The parser treated compatibility scaffolding like required route metadata. That made an unused proof capable of failing initial mount, SSR, and client navigation paths.

Default invalid compatibility metadata to the unknown proof for Wave01, document the planned hard-fail point for #726-COMPAT-04/05, and cover malformed, future-version, and non-object payloads.
2026-05-06 08:15:26 +01:00
James Anderson a1d2b61034 refactor(server): dedupe HTTP error response builders (#1087)
* refactor(server): dedupe HTTP error response builders

Extract notFoundResponse / badRequestResponse / internalServerErrorResponse /
methodNotAllowedResponse into a shared http-error-responses.ts helper,
alongside the existing forbiddenResponse (moved from request-pipeline.ts) and
payloadTooLargeResponse (moved from app-server-action-execution.ts). Sites
with custom bodies (e.g. "404 - API route not found", "Image not found",
"missing pattern", "Page has no default export") and template-string-embedded
sites in deploy.ts / pages-server-entry.ts / dev-origin-check.ts are left
inline. Follow-up to #1058.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: retrigger workflows

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:34:23 +00:00
James Anderson 63b59a2cc8 refactor(routing): unify route-match preamble across pages and app routers (#1086)
- routing: extract a shared generic matchRouteWithTrie<R> in
  route-matching.ts and use it from pages-router.ts (matchRoute) and
  app-router.ts (matchAppRoute). Both now share the same query-strip,
  trailing-slash, normalize, split, and trie-cache lookup preamble.

Pure refactor; no behavior change. Follow-up to #1058.

Note: the planned migration of pages applyGsspHeaders to the shared
mergeMiddlewareResponseHeaders helper was deferred — applyGsspHeaders
reads Node-style OutgoingHttpHeaders (Record<string, string | number |
boolean | string[]>) and joins multi-value non-cookie arrays with ", "
before set(), neither of which the shared Headers-based helper does.
Collapsing without preserving those semantics risks behavioral drift,
so it'll ship as a separate PR.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:28:09 +01:00
James Anderson b67eafd89c refactor(utils): dedupe trailing-slash and basePath normalization (#1084)
* refactor(utils): dedupe trailing-slash and basePath normalization

Add removeTrailingSlash to base-path.ts and migrate ad-hoc replace
patterns + ad-hoc basePath checks to the centralized helpers.
Follow-up to #1058.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(utils): use char-code loop in removeTrailingSlash

Replace the /\/+$/ regex with a manual trailing-slash count to avoid a
CodeQL polynomial-regex-on-uncontrolled-data alert. Same behaviour, no
backtracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: retrigger workflows

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:26:16 +00:00
James Anderson ed11b3a576 refactor(server): dedupe TextDecoder stream consumption (#1083)
* refactor(server): dedupe TextDecoder stream consumption

Extract the repeated `new TextDecoder()` + `ReadableStream` chunk-loop
into shared helpers in `utils/text-stream.ts`:

- `readStreamAsText(stream)` for the consume-to-single-string pattern
  (used in `app-page-cache.ts` ISR write and `pages-page-response.ts`
  ISR cache write).
- `readStreamAsTextWithLimit(stream, maxBytes, onLimitExceeded)` for
  the size-limited pattern (used in `app-server-action-execution.ts`
  and `pages-node-compat.ts`). Each caller passes its own throwing
  callback so error types (`Error` vs `PagesBodyParseError`) and
  status codes are preserved.

Sites with additional load-bearing logic intentionally left inline:
- `rsc-stream-hints.ts` (line-buffered TransformStream with carry)
- `app-ssr-stream.ts` (raw-byte accumulator + flight hint fix; HTML
  flush transform with `fixPreloadAs`)
- `shims/fetch-cache.ts` (size-limited cache-key body canonicalisation
  with mixed string/Uint8Array stream and tee handling)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(server): address review feedback from #1083

- Dedupe `readAppPageTextStream` in `app-page-execution.ts` (called
  once from `app-page-dispatch.ts`); replace with `readStreamAsText`
  and remove the now-unused export. The deduped helper additionally
  releases the reader lock in `finally`, which the inline copy did
  not — a robustness improvement on a wholly-consumed stream.
- Update `tests/app-page-execution.test.ts` to use `readStreamAsText`.
- Mirror the `try/finally { reader.releaseLock() }` pattern in
  `readStreamAsTextWithLimit` for consistency with `readStreamAsText`.
  The happy path implicitly releases when the stream completes; the
  over-limit path cancels which also releases. The explicit release
  prevents lock leaks if a future caller catches the limit error and
  reuses the reader handle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:24:09 +01:00
James Anderson 32e80223a3 chore(shims): delete unused deprecated exports (#1082)
- Remove `toRscUrl` from shims/navigation.ts (zero internal callers;
  all sites use createRscRequestUrl).
- Remove matching declaration from shims/next-shims.d.ts.
- Update stale comment in server/app-browser-entry.ts that referenced
  toRscUrl by name.

Note: the originally-scoped `requestAsyncStorage` legacy alias in
shims/internal/work-unit-async-storage.ts was kept. It is the named
export that resolves through the
`next/dist/client/components/request-async-storage.external` import-path
mapping (see packages/vinext/src/index.ts and check.ts), and is asserted
by tests/shims.test.ts. The shim's JSDoc documents `@sentry/nextjs`
runtime-resolves request context via this name, so removing it would
break that ecosystem path.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:20:02 +01:00
Nathan Nguyen 4342311377 test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour (#1053)
* test(app-router): lock navigation lifecycle settlement and root-layout hard navigation behaviour

Adds 13 unit tests covering two Wave 0 baseline areas from issue #726:

BASE-02 — Lock current lifecycle settlement behaviour (9 tests):
- Three concurrent navigations with reverse-order payload resolution
- Rapid sequential navigation: stale deferred payload does not overwrite
- Stale cross-root navigation is skipped instead of hard-navigating
- resolvePendingNavigationCommitDisposition exhausts skip/dispatch/hard-navigate
- resolveAndClassifyNavigationCommit classifies skip when IDs diverge
- Failed RSC payload settles pending router state cleanly

BASE-03 — Lock root-layout hard navigation behaviour (4 tests):
- renderNavigationPayload calls window.location.assign on root change
- Hard-navigate settles the pending router state before navigating away
- shouldHardNavigate negative/null path exhaustiveness
- shouldHardNavigate positive path with distinct roots

These tests close the App Router concurrency gap: the Pages Router has 10
race-condition tests in shims.test.ts but the App Router (which uses a
completely different lifecycle mechanism) had zero equivalent tests for
out-of-order payload resolution and stale commit rejection.

* test: remove hallucinatory comments from app-browser-entry tests

* test: remove duplicate shouldHardNavigate null-root assertion

* test: remove low-signal helper truth-table tests, fix hard-navigate ordering assertion

---------

Co-authored-by: James <james@eli.cx>
2026-05-05 19:09:33 +00:00
Nathan Nguyen d6c270f6b8 fix: filter internal Next.js headers from inbound requests (#1056)
* fix: filter internal Next.js headers from inbound requests

Next.js strips internal headers (x-nextjs-data, x-matched-path,
x-now-route-matches, x-next-resume-state-length, and x-middleware-*)
via filterInternalHeaders() at the router-server entry point before
any handler or middleware sees the request. This prevents attackers
from forging these headers to influence routing or impersonate
internal data fetches.

vinext was not filtering any of these headers from inbound requests,
so all four reached middleware as user-controlled request headers.

Ported from Next.js INTERNAL_HEADERS list and filterInternalHeaders():
- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/server-ipc/utils.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-server.ts

Added filterInternalHeaders() to server/request-pipeline.ts and wired
it into all request entry points:
- App Router handler (app-rsc-handler.ts)
- CF Worker App Router entry (app-router-entry.ts)
- Node prod-server (nodeToWebRequest + Pages Router path)
- Dev server Pages Router middleware path (index.ts)
- Generated Pages Router worker entry (deploy.ts)

Filtering runs before middleware, config matching, and routing so no
subsystem sees forged internal headers. The x-middleware-* headers are
stripped from requests but NOT from middleware responses — the
middleware protocol (which reads x-middleware-* from Response headers,
not request headers) continues to work correctly.

* fix: use safe Request rebuild instead of mutating headers directly

Request.headers is immutable in Cloudflare Workers / Miniflare /
workerd environments. The previous in-place mutation via
headers.delete() threw 'Can't modify immutable headers', causing
every request to 500 in the cloudflare-dev E2E suite.

Changed filterInternalHeaders() to always return a new Headers
copy (never mutates the input). Call sites that use request.headers
now rebuild the Request with the filtered headers, matching the
existing pattern in applyMiddlewareRequestHeaders() at
config-matchers.ts:495-502.

* fix: clone requests when filtering headers

* fix: clone Requests safely across runtimes

* fix: avoid type assertions in Request cf access

* fix: move header filtering from handler body to entry boundary

handleAppRscRequest() is an internal handoff point, not a trust
boundary. Filtering here broke the x-vinext-mw-ctx handoff in hybrid
app+pages dev mode because cloned Request objects could drop
coordination state from the connect handler, causing middleware to
run twice.

Moved filtering to the top of createAppRscHandler(), the true App
Router request boundary, before headersContextFromRequest() and
requestContextFromRequest() capture any context. This also fixes the
security gap where forged headers were captured in the request context
before filtering ran.

* fix: preserve x-vinext-mw-ctx when filtering internal headers in app-rsc-handler

srvx's NodeRequestHeaders reads from rawHeaders for iteration but falls back
to req.headers for .get() / .has(). In the dev server we add x-vinext-mw-ctx
to req.headers after the Request is built, so it is visible to .get() but lost
when filterInternalHeaders iterates. Read it BEFORE iterating so
applyForwardedMiddlewareContext can skip middleware and avoid double execution.

Fixes CI failure on PR #1056.

* fix: copy cf in both clone paths, add cloneRequestWithHeaders tests

cloneRequestWithHeaders now copies the Workers cf property in both
the primary (try) path and the fallback path. Added 8 unit tests
locking down method, URL, redirect, signal, body, cf, and combined
metadata preservation to prevent regression to the broken
URL-constructor pattern.
2026-05-05 20:03:05 +01:00
James Anderson 0478e4fc51 refactor(config): dedupe regex cache and destination param chain (#1080)
Extract the three internal regex compilation caches in
config-matchers.ts to a single getCachedRegex helper, and unify the
substituteDestinationParams + sanitizeDestination chain used by
redirects and rewrites. Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:55:06 +01:00
James Anderson d2da43b42f refactor(routing): dedupe findFileWithExtensions helper (#1076)
* refactor(routing): dedupe findFileWithExtensions helper

Extract the matcher.dottedExtensions.some(existsSync) one-liner used
by both prerender.ts and dev-server.ts into routing/file-matcher.ts.
Follow-up to #1058.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): drop stale ValidFileMatcher type import

The type is no longer used after delegating to the shared
findFileWithExtensions helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:54:05 +01:00
James Anderson 604d548457 refactor(als): dedupe AsyncLocalStorage globalThis registration (#1081)
Extract the `Symbol.for(...) + globalThis ??= new AsyncLocalStorage()`
pattern from headers.ts, cache.ts, cache-runtime.ts, fetch-cache.ts,
request-context.ts, unified-request-context.ts, and the four *-state.ts
shims into a single helper at shims/internal/als-registry.ts.

Cross-bundle singleton property is preserved: the helper still routes
through `Symbol.for(key)` (global symbol registry) and `globalThis[sym]
??= ...`, so multiple instances of the helper module — and multiple
instances of each call site — all converge on the same ALS for a given
key. The helper holds no module-local reference of its own.

Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:53:25 +01:00
James Anderson b9769cc530 refactor(entries): drop dead locale helpers and dedupe path normalization (#1079)
- Delete unused extractLocale / detectLocaleFromHeaders /
  parseCookieLocaleFromHeader (the canonical implementations live in
  server/pages-i18n.ts and resolvePagesI18nRequest is what the entry
  actually calls).
- Extract the repeated `.replace(/\\/g, "/")` path-separator
  normalization across the entry files into a shared helper.

Follow-up to #1058 and #1069.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:51:06 +01:00
James Anderson 3e00dc8953 refactor(server): dedupe action/page/route-handler error response building (#1078)
Extract the "Payload Too Large" 413 response and NEXT_REDIRECT /
NEXT_NOT_FOUND / NEXT_HTTP_ERROR_FALLBACK digest parsing into shared
helpers. Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:50:22 +01:00