Commit Graph

1503 Commits

Author SHA1 Message Date
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
James Anderson 874ebef1bd refactor(check): dedupe status sort and App Router file filters (#1075)
Extract the repeated status-order comparator and the parallel
page/layout/loading/error file-type predicates in check.ts into
shared helpers. Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:49:00 +01:00
James Anderson 05933cb1c8 refactor(build): dedupe JSON file safe-read pattern (#1077)
Extract the repeated `try { JSON.parse(readFileSync(...)) } catch`
pattern from standalone.ts and server-manifest.ts into a shared
readJsonFile helper. Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:48:13 +01:00
James Anderson 64fea36aac refactor(server): dedupe .rsc suffix stripping (#1073)
Extract the repeated `pathname.replace(/\.rsc$/, "")` and equivalent
slice-based suffix removal into a single `stripRscSuffix` helper.
Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:47:45 +01:00
James Anderson 30775c8cfa refactor(plugins): dedupe og-asset read-to-base64 wrapper (#1074)
Extract the repeated try/catch file-read + base64 encode pattern
in og-assets.ts into a shared helper. Follow-up to #1058.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:47:20 +01:00
Nathan Nguyen 634808bea4 perf(pages): cache ISR misses from streamed render (#1068)
* perf(pages): cache ISR misses from streamed render

Pages Router ISR cache misses currently render the page once for the live response and then render a second React tree to produce cache HTML. That doubles userland render, data reads, suspense work, and render-scoped side effects on every cold cache fill.

The cache writer now tees the already-rendered body stream, records the cache branch with streaming UTF-8 decoding, and writes the same shell and body that the client receives. Stream failures report through the Pages Router render error path and never persist partial HTML.

Tests cover the single-render cache fill, split UTF-8 chunks, failed stream writes, and the Pages Router fixture state contract.

* fix(pages): keep ISR cache error reporting non-fatal

* chore: rerun CI
2026-05-05 19:43:07 +01:00
Nathan Nguyen a66a62da4c perf(app-router): coalesce client reference preloads (#1067)
Cold App Router SSR requests currently share Vite RSC's underlying module import promises, but each request can still iterate the full client-reference manifest and allocate its own Promise.all fan-out before the completed boolean flips. That keeps first-request correctness but wastes work during cold bursts.

The preload state only tracked completion, not in-flight work. Concurrent callers had no shared promise to await, and the preload logic lived inline in the SSR entry where the concurrency contract was hard to test.

Add a typed client-reference preloader that shares the global manifest preload promise, dedupes per-reference work for future scoped manifests, preserves non-fatal reference load reporting, and keeps handleSsr awaiting the preload before React consumes the Flight stream.
2026-05-05 19:39:39 +01:00
Nathan Nguyen 5a18d36bfd feat(app-router): add navigation trace reason-code shell (#1063)
* feat(app-router): add navigation trace reason-code shell

Navigation commit classification can now expose compact reason-code traces for the existing stale, root-boundary, unknown-root, and dispatch outcomes. The plain disposition-only path remains allocation-light for the browser navigation hot path.

The old implicit string result made it hard for follow-up lifecycle work in #726 to attach structured operational context without inventing narrative logs at each call site. This adds a narrow trace contract and threads it through the same-URL classification helper while preserving current commit behavior.

Covered by app-browser-entry tests for stale commit skips, root-boundary hard navigation, unknown root-layout fallback, same-URL classification traces, and trace field ownership.

* fix(app-router): tighten navigation trace shell

The OPS-01 trace shell exposed an append helper before any source path needed multi-entry traces, typed reason-code values too loosely, and let the combined same-URL classifier allocate a trace that the controller never consumed.

Remove the unused append helper, derive reason-code unions from a literal key-to-code map, keep the classifier on the allocation-light disposition path, and add the missing normal dispatch trace coverage.
2026-05-05 19:27:19 +01:00
James Anderson 61a4551bee refactor(shims): dedupe cookie value serialization (#1072)
* refactor(shims): dedupe cookie value serialization

Extract the shared encodeURIComponent-based value encoding from
headers.ts and server.ts ResponseCookies into a single helper.
Follow-up to #1049.

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

* fix(shims): drop unused export of SerializeSetCookieOptions

knip flagged this as an unused export — the type is only consumed
within cookie-serialize.ts itself.

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:24:21 +01:00
James Anderson bdc1f34c35 refactor(server): dedupe forbidden response construction (#1071)
Extract the repeated `new Response("Forbidden", { status: 403, ... })`
pattern from dev-origin-check.ts and request-pipeline.ts into a single
shared helper. Follow-up to #1049.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:23:50 +01:00
James Anderson 728b8e3b76 refactor(i18n): dedupe NEXT_LOCALE cookie parsing (#1069)
Remove the dead `parseCookieLocaleFromHeader` helper from the Pages SSR
entry template. It duplicated the regex + decodeURIComponent pattern
already exported from `pages-i18n.ts` (used by `resolvePagesI18nRequest`)
and had no callers in the generated server entry — locale resolution
goes through `resolvePagesI18nRequest` which handles the cookie itself.

Follow-up to #1049.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:23:22 +01:00
James Anderson 9997ee7200 refactor(app-browser-entry): dedupe encoded JSON header parsing (#1070)
Extract the repeated try/catch JSON.parse(decodeURIComponent(...))
block into a single helper. Follow-up to #1049.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:22:41 +01:00
Nathan Nguyen c1a4c8da1a fix(routing): accept any non-] chars in dynamic segment names (Next.js parity) (#1054)
Next.js PARAMETER_PATTERN accepts any non-] characters inside brackets:
  [repo:name], [variant.id], [user@domain]

vinext was restricting param names to [\w-]+, silently treating segments
with dots, colons, or @ as literal URLs. This fix aligns the regexes with
Next.js:

- app-route-graph.ts: ^\[([^\]]+)\]$ (was [\w-]+)
- pages-router.ts: same
- route-validation.ts: patternToNextFormat now backtracks over +/* suffixes
- shims/router.ts: extractRouteParamNames now accepts dotted/colon names

Also corrects a misleading comment in routePrecedence: dynamic routes CAN
score negative, but the trie matcher (route-trie.ts) always checks static
children before dynamic children at each node, so purely-static routes still
win at request time.

Next.js reference:
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-dynamic-param.ts#L175

Closes #7
2026-05-05 19:21:40 +01:00
Nathan Nguyen e216dab483 refactor(app-router): introduce AppElementsWire boundary (#1059)
* refactor(app-router): introduce AppElementsWire boundary

App Router payload ownership currently leaks through raw helper calls for route ids, cache keys, metadata writes, and decode paths. That makes the flat AppElements transport act like router semantics instead of a narrow wire boundary, which violates the #726-WIRE-01 ownership target.

Introduce AppElementsWire as the single codec facade for AppElements wire encoding, decoding, metadata creation, and outgoing payload shaping. Route the current production encode/decode/write call sites through that facade while keeping the legacy helper names re-exported only where existing tests cover compatibility.

* chore(app-router): address AppElementsWire review
2026-05-05 19:15:27 +01:00
Nathan Nguyen 834ccc6ce7 feat(app-router): mint semantic route graph ids (#1060)
App Router route graph entries previously left route, page, layout, template, and slot identity to later render and wire layers. That kept topology meaning coupled to reconstructed payload keys instead of a graph-owned fact.

Mint stable semantic IDs during graph construction, preserve them in generated RSC route metadata, and cover route-group, dynamic-segment, slot-owner, and filesystem-root stability cases.
2026-05-05 19:08:00 +01:00
Nathan Nguyen 57e6b7001e feat(app-router): track visible commit versions for browser operations (#1061)
Browser router state changed without a versioned visible commit baseline or a typed operation record, leaving later stale-result gates without a stable comparison point.

Add pending and committed OperationRecord metadata, thread explicit operation lanes through browser navigation, refresh, traverse, same-URL server actions, and HMR, and advance visibleCommitVersion through the reducer path that accepts a new visible router-state commit.

Also harden the prefetch-cache unit test to wait for async router.prefetch setup rather than relying on a fixed scheduler tick.
2026-05-05 19:05:51 +01:00
Nathan Nguyen 2bd7377f70 fix(shims): match Next metadata merge and Twitter inheritance (#1055)
* fix(shims): deep-merge nested metadata and add OG/Twitter inheritance

mergeMetadataEntries did a flat top-level key replacement, so setting
openGraph: { title: "Override" } in a child layout erased all root
OpenGraph fields like siteName and images. Next.js resolves these
per-subkey; we now shallow-merge the nested objects for openGraph,
twitter, icons, alternates, robots, and other.

Setting only openGraph.title without twitter.title produced no Twitter
Card tags because vinext skipped Next.js's postProcessMetadata step.
We now auto-fill twitter:title from og:title (or metadata:title),
twitter:description from og:description (or metadata:description),
and twitter:images from og:images, and default twitter:card to
summary_large_image when images are present.

Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolve-metadata.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts

* fixup! fix(shims): deep-merge nested metadata and add OG/Twitter inheritance

Extract postProcessMetadata from mergeMetadataEntries to avoid
polluting intermediate merge results during layout accumulation.
Run it once at the end of resolveAppPageHead after file-based
metadata has been applied, matching Next.js's accumulator pattern.

Also fix two issues caught by Copilot review and CI:

1. Twitter auto-fill now works even when openGraph is absent —
   metadata.title and metadata.description serve as fallback sources.

2. Remove all as assertions from production code; use typed locals
   and direct index access via Metadata's string index signature.

3. Update app-page-head.test.ts expectations to match post-processed
   output and add missing regression test for no-openGraph case.

* chore: address review feedback — clone input, ?? for title, missing test

- postProcessMetadata now shallow-clones its input to prevent accidental
  mutation of the caller's object.
- resolveStringTitle uses ?? instead of || to avoid treating empty
  string as falsy (semantically correct but practically no difference).
- Added comment explaining why verification, appleWebApp, appLinks,
  and formatDetection are excluded from DEEP_MERGE_KEYS.
- Added test: child can clear a parent openGraph subkey by setting it
  to undefined, verifying twitter auto-fill skips cleared images.

* fix(shims): match Next metadata segment replacement

Nested metadata objects were being merged across segments, which kept stale parent OpenGraph, Twitter, alternates, icons, and robots fields when a child segment defined the same key.

Next only merges custom other metadata across segments. Replace the broad nested merge with an explicit other merge, keep OG-to-Twitter post-processing gated by openGraph, and update metadata tests to lock in the compatibility contract.
2026-05-05 19:00:10 +01:00
Nathan Nguyen 68c90808e0 fix(routing): discover nested parallel slot sub-routes from layout-only parents (#1051)
* fix(routing): discover nested parallel slot sub-routes from layout-only parents

Nested parallel routes failed when the parent segment had a layout but no
page.tsx. discoverSlotSubRoutes skipped any parent route without pagePath,
so slots like @parallelB/nested/page.tsx under home/layout.tsx (but no
home/page.tsx) were never discovered.

Next.js explicitly tests this shape in parallel-routes-and-interception:
app/parallel-nested/home/layout.tsx + @parallelB/default.tsx +
@parallelB/nested/page.tsx creates a valid /parallel-nested/home/nested
route.

The fix makes three targeted changes to discoverSlotSubRoutes:

1. Remove the hard pagePath requirement. Layout-only routes with parallel
   slots can still own nested slot sub-routes.

2. Compute parentPageDir from the innermost layout when pagePath is null,
   so slot ownership checks still work for layout-only routes.

3. Relax the children default.tsx requirement, but only for layout-only
   parent routes. When a parent HAS a children page, default.tsx is still
   required as a fallback for the synthetic sub-route. Layout-only parents
   do not need this fallback because the children slot was never occupied.

4. Add structural-conflict detection before creating synthetic routes. This
   prevents layout-only root routes from generating synthetic routes that
   collide with existing page routes differing only by param name (e.g.
   /shop/:id vs /shop/:name), which validateRoutePatterns rejects.

Tests ported from the Next.js parallel-routes-and-interception suite cover
both route-discovery and end-to-end rendering.

Refs: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts

* fix(routing): correct patternsStructurallyEquivalent tree-node comparison

The initial implementation of patternsStructurallyEquivalent used a naive
dynamic-segment check (startsWith(':')) that produced two categories of
false positives:

1. Literal vs dynamic segments (e.g. 'about' vs ':id') were considered
   equivalent, incorrectly suppressing valid synthetic routes.

2. Single dynamic vs catch-all segments (e.g. ':id' vs ':slug+') were
   considered equivalent, even though they map to different validator tree
   nodes ([] vs [...]).

Replace the ad-hoc boolean logic with segmentTreeNodeType, which classifies
each segment by its Next.js validator tree node (literal, dynamic, catchAll,
optionalCatchAll). Two patterns are structurally equivalent only when every
segment maps to the same tree node type.

Also add a clarifying comment to the parentPageDir computation so the next
developer understands why the innermost layout proxies the route directory
for layout-only routes.

* fix(routing): scan synthetic routes in structural conflict check

Copilot review identified that patternsStructurallyEquivalent only scanned
the original routes array, missing conflicts between synthetic routes created
earlier in the same loop. Fixed by scanning routesByPattern.values() which
includes both original and synthetic routes.

Also adds a TODO comment documenting the known prerender limitation for
layout-only synthetic routes (skipped at build/prerender.ts:849). These
routes work in dev/SSR but won't be statically exported — this is
pre-existing behaviour for all layout-only routes.

* test(routing): cover structural conflict skip for synthetic parallel routes

Adds a unit test proving that discoverSlotSubRoutes skips synthetic routes
when an existing page route has the same structural shape but a different
param name (e.g. /shop/:id page + @feed/[name]/page.tsx slot).

Without this coverage the structural-conflict safeguard is easy to regress.

* fix(routing): guard discoverSlotSubRoutes to layout-only UI routes

Review identified that removing the old !parentRoute.pagePath guard also
let route-handler-only AppRoutes enter discoverSlotSubRoutes. Route
handlers have pagePath: null but are not layout-only UI routes. A shape
like app/api/route.ts + app/@feed/foo/page.tsx could make the /api route
handler scan the ancestor slot and materialise a synthetic UI route under
/api/foo.

Add an explicit isLayoutOnlyUiRoute check:
  !parentRoute.pagePath && !parentRoute.routePath && parentRoute.layouts.length > 0

Only page-bearing routes or layout-only UI routes can own nested parallel-
slot sub-routes. Route handlers and layout-less routes are skipped.

Also adds a regression test: a route.ts handler with an ancestor parallel
slot must not emit synthetic routes under the handler pattern.

* fix(routing): scan synthetic routes in structural conflict check + regression test

The structural-conflict guard had regressed back to scanning the original
routes array, missing conflicts against synthetic routes created earlier in
the same discoverSlotSubRoutes pass. Fixed by scanning
Array.from(routesByPattern.values()) instead.

Also adds a regression test proving that two slot sub-pages with different
param names under the same parent do not both materialise (e.g. @a/[id] and
@b/[name] under /shop — only one of /shop/:id or /shop/:name is emitted).
2026-05-05 18:30:50 +01:00
James Anderson 7b41e950bd fix(app-page): align redirect()/notFound() handling under loading.tsx with Next.js (#1057)
* fix(app-page-probe): always await page promise so redirect()/notFound() under loading.tsx return 307/404

The probe used to fire-and-forget the page promise when a route-level
loading.tsx was present (`awaitAsyncResult: !hasLoadingBoundary`). For
async pages that throw `redirect()` or `notFound()`, that swallowed the
rejection during probe; the RSC stream then re-ran the page under the
route Suspense boundary, where React converted the throw into a
"Switched to client rendering" error serialized into a 200 HTML body
instead of a clean 307/404.

Awaiting the page in the probe surfaces the special error early, parity
with the no-loading-boundary path. The cost is that the outer page
function runs serially with the RSC render rather than in parallel — in
practice most async work in `loading.tsx`-shielded routes happens in
nested Suspense children, not in the outer page function body.

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

* refactor: capture special-error digests post-render instead of awaiting probe

Replaces the previous "always await page in probe" approach with a
post-render swap that mirrors Next.js's mechanism in app-render.tsx:
the page render's onError captures NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK
digests; after the HTML stream drains, the lifecycle inspects the
tracker and swaps a 307/404 in place of the streamed "Switched to
client rendering" body.

This avoids the 2× page invocation cost of awaiting in the probe (the
previous fix called the page once during probe and again during RSC
render, serially). Now the page runs once, inside the RSC render, and
the route-level Suspense boundary (loading.tsx) traps the throw as
designed. The buffered stream still contains the same final HTML for
non-redirecting pages.

Trade-off matches Next.js: loading.tsx fallback visibility is sacrificed
for routes that redirect (you cannot both flush the loading state and
reverse course to a 307). Non-redirecting routes with loading.tsx now
buffer the full response rather than streaming progressively — same
trade-off Next.js makes with its prerender-based pipeline.

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

* Revert "refactor: capture special-error digests post-render instead of awaiting probe"

This reverts commit 179b20f88033f3731677a91faeb022389247223e.

* refactor: align with Next.js — skip page probe + post-shell race for special errors

Replaces the always-await fix with a Next.js-aligned pipeline:

- Skip the page probe entirely when a route-level loading.tsx is present.
  The page now runs once, inside the RSC render, instead of twice serially.
- Capture NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK digests in the
  rscErrorTracker via React's onError callback (previously dropped).
- After the SSR shell is ready but before bytes are flushed, race the
  captured digest against a 50 ms swap window. A digest that fires within
  the window — sync throws and short-async like ~10 ms auth checks — is
  converted to a 307/404. Late rejections fall through to the streamed
  body, matching Next.js's "until-first-byte-is-flushed" semantic.

Trade-offs vs. the previous always-await fix:
- Page invoked once instead of twice (no more 2× latency for async outer
  page functions).
- TTFB for loading.tsx routes goes from ~outer-page-time to ~50 ms.
- loading.tsx fallback is visible immediately after the swap window
  closes — the OpenNext-compat E2E that broke under the buffer-everything
  approach passes.

Slow async redirects (>50 ms) still leak as serialized stream errors.
Next.js handles that case by injecting <meta http-equiv="refresh">
mid-stream; doing the same in vinext is a separate, larger change.

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

* refactor: drop 50ms swap-window — deterministic post-shell digest check

Removes the timer-race hack and replaces it with the same shape Next.js
uses (`app-render.tsx:4293`): after the SSR shell promise resolves,
inspect the rscErrorTracker for a captured NEXT_REDIRECT/NEXT_HTTP_ERROR_FALLBACK
digest and swap the response to a 307/404 if one is present.

Why this works without a timer: the SSR pipeline naturally runs many
microtask awaits between the page Promise rejecting (in onError) and
the lifecycle reading the tracker. Throws that resolve in microtasks
during shell rendering are deterministically captured. Throws that
require macrotask boundaries (real I/O, setTimeout) are NOT caught and
fall through to the streamed body — exactly the same trade-off Next.js
makes (the digest survives in the Flight payload for the client router
to consume).

Updates the regression fixture to use the deterministic case (sync
throw inside an async page) instead of the pre-fix repro's setTimeout
trick. The setTimeout case can't be deterministically caught without
either a timer (rejected as a hack) or React-internals patches.

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

* test: regression for permanentRedirect() under loading.tsx returning 308

Adds a fixture and integration test mirroring the redirect()-with-
loading.tsx case but using permanentRedirect (308). Confirms the
post-shell digest swap honors the statusCode field from the NEXT_REDIRECT
digest rather than coercing to the 307 default — closes a flagged
gap from the Next.js parity audit.

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

* test: regression for forbidden()/unauthorized() under loading.tsx

Adds fixtures and integration tests verifying the post-shell digest
swap correctly preserves the status code from NEXT_HTTP_ERROR_FALLBACK
digests:
  - forbidden() under loading.tsx → 403 with root forbidden.tsx body
  - unauthorized() under loading.tsx → 401 with root unauthorized.tsx body

Confirms resolveAppPageSpecialError reads `parseInt(parts[1], 10)` from
the digest rather than coercing to 404 — closes the parity-audit's
"forbidden/unauthorized status coercion" gap (which turned out to
already be handled correctly; this just locks it in).

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

* fix(app-page): apply basePath to redirect Location header

Mirrors Next.js's `addPathPrefix(getURLFromRedirectError(err), basePath)`
in app-render.tsx — `redirect("/about")` from a page mounted under
basePath "/blog" now produces `Location: /blog/about` instead of the
raw "/about".

- Adds `basePath` to BuildAppPageSpecialErrorResponseOptions and a small
  helper that resolves the redirect target against the request URL and
  prefixes app-internal absolute paths.
- External redirects (different origin) and targets that already start
  with the basePath are passed through unchanged.
- Threads the entry-generated `__basePath` through dispatch and into
  both the page and layout special-error response builders.
- Closes a parity gap flagged in the Next.js audit; covered by 5 new
  unit assertions exercising internal / external / already-prefixed /
  unconfigured / root-target cases.

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

* fix(app-page): preserve cookies().set() on redirect responses

Mirrors Next.js's `appendMutableCookies(headers, requestStore.mutableCookies)`
in app-render.tsx. Auth flows that do
  cookies().set("session", "...");
  redirect("/dashboard");
must preserve the Set-Cookie on the 307, otherwise the redirected
request lands without the just-issued session and the user bounces
back to login.

- Adds an optional `getAndClearPendingCookies` to BuildAppPageSpecialErrorResponseOptions
  that, when set, drains pending Set-Cookie values accumulated during
  the render and appends them to the redirect response.
- Wires `getAndClearPendingCookies` from `vinext/shims/headers` into both
  the page and layout special-error response builders in dispatch.
- Only applied to redirect responses to match Next.js — http-access-fallback
  responses leave cookies to the rendered boundary.
- Closes a parity gap from the audit; covered by a new unit test
  exercising redirect-with-cookies, redirect-without-cookies, and the
  http-access-fallback-cookies-not-bled cases.

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

* test: regression for notFound() under loading.tsx

Adds a fixture and integration test covering the most common
loading-boundary special-error case in real apps: a dynamic detail
page with a loading state that calls notFound() when the record is
missing.

Distinct from the forbidden()/unauthorized() coverage — notFound()
throws the bare "NEXT_NOT_FOUND" digest (not "NEXT_HTTP_ERROR_FALLBACK;404"),
which takes a separate branch in resolveAppPageSpecialError. This locks
in that the post-shell digest swap handles both branches.

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 16:46:55 +00:00
Nathan Nguyen 31b3e17843 fix(thenable-params): protect well-known properties and unify page/route handler implementation (#1050)
Route handlers used a local Object.assign(Promise.resolve(...), params)
helper that did not protect any well-known properties from shadowing.
Page params used a Proxy but only guarded then/catch/finally, missing
React's Promise status field and other well-known properties.

Next.js explicitly reserves a well-known property set (then, catch,
finally, status, toString, hasOwnProperty, etc.) so that params with
those names do not break Promise behaviour or React introspection.

This change:
- Expands the shared makeThenableParams shim to protect the full
  well-known property set, matching Next.js reflect-utils.ts.
- Replaces the weak route-handler local helper with the shared shim.
- Adds unit tests for status and Object.prototype method shadowing.
- Adds App Router integration tests for pages and route handlers with
  params named then, catch, finally, and status.

Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils/reflect-utils.ts
- https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components/cache-components.params.test.ts
2026-05-04 19:30:05 +01:00
James Anderson 7f4a43dcf4 refactor(routing): dedupe param decoding into shared helper (#1058)
Extract the identical `decodeParam`/`decodeParams` and
`decodePatternParam`/`decodePatternParams` helpers from `route-trie.ts`
and `route-pattern.ts` into a single `decodeMatchedParams` in
`routing/utils.ts`. Addresses review feedback on #1049.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:11:10 +00:00
Nathan Nguyen 6c786d69a9 fix(routing): decode matched params with decodeURIComponent (#1049)
* fix(routing): decode matched params with decodeURIComponent

Vinext's segment-safe normalization preserves encoded delimiters (%2F) for
correct route matching, but the captured param values were left undecoded.
This caused dynamic params to return "a%2Fb" instead of "a/b", diverging
from Next.js which decodes at the route-matcher level.

Add decodeURIComponent pass in trieMatch and matchRoutePattern — the two
route matching entry points — so all downstream consumers (pages, route
handlers, metadata, useParams) receive decoded param values.

Mirrors Next.js: packages/next/src/shared/lib/router/utils/route-matcher.ts L25-27

* test(app-rsc-route-matching): update param decode assertion to match new behavior
2026-05-04 18:58:48 +01:00
Nathan Nguyen 390b4608be fix(server): reject invalid HTTP methods with 400 in app route handlers (#1048)
Vinext previously uppercased the request method and fell through to the normal 405 response for any unrecognized method. Next.js rejects non-standard methods with 400 before dispatch.

- Add isValidHTTPMethod() predicate to app-route-handler-runtime.ts

- Return empty 400 in dispatchAppRouteHandler() before auto-OPTIONS/405

- Port Next.js test: HEADER => 400 Bad Request

Next.js source reference:

https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/app-route/module.ts#L390-L392

https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-routes/app-custom-routes.test.ts#L531-L538
2026-05-04 18:56:23 +01:00