Commit Graph

16 Commits

Author SHA1 Message Date
Nathan Nguyen 1a480016f6 fix(app-router): track searchParams access for static bailout (#1788)
* fix(app-router): track searchParams access for static bailout

App Router pages that awaited or used the searchParams prop during prerender could still be classified as static when the prerender request had an empty query string. That seeded no-query output into the deploy cache and served it for requests like ?search=hello.

The dynamic decision was based on query-string content instead of page prop access. Observe the searchParams thenable itself, including React use() status reads, and mark searchParams dynamic only when the prop is consumed.

* chore: rerun ci

* fix(app-router): avoid static cache for searchParams pages

* fix(app-router): tolerate synthetic page paths in manifest codegen

* fix(app-router): observe searchParams access for static bailout

* fix(app-router): preserve app page cache hits

* fix(app-router): observe loading search params during render

* refactor(app-router): extract searchParams probe fan-out into helper

Move the per-request page probe fan-out (matched page + parallel slot
pages + interception page) out of the generated RSC entry and into a
unit-testable buildAppPageProbes() helper in server/app-page-probe.ts,
keeping entries/app-rsc-entry.ts thin (AGENTS.md guidance). Addresses
bonk review concern #3 on #1788.

Document the known slot over-bail (probing inactive parallel-route slot
pages can mark an otherwise-static page dynamic; fails safe toward
dynamic) and track it as a caching-efficiency follow-up in #1798
(bonk review concern #1).

* fix(app-router): skip probing interception-overridden slot pages

Narrow the searchParams probe fan-out so it only probes page components
that actually render for the request. When an interception matches, it
replaces the page of the slot named by intercept.slotKey (the element
builder sets overrides[slotKey].pageModule to the interception page,
which wins over slot.page in app-page-route-wiring.tsx). We now probe
the interception page in place of that slot's own page instead of
probing both — probing the overridden slot page marked an otherwise
static request dynamic for a component that never renders.

Fixes the over-bail flagged in review of #1788.

* docs(app-router): clarify buildAppPageProbes slot precision

Address review feedback: the doc comment overstated precision without
explaining why non-overridden slots are exact. Spell out that a slot
with a page.tsx always renders that page (so probing it is correct),
default-only slots have no slot.page.default so probing is a no-op
(not an over-bail), and a default.tsx awaiting searchParams is
backstopped by the real render's observation (so no under-bail).

* fix(app-router): gate searchParams interception probe on isRscRequest

Interception only fires for RSC navigations (resolveAppPageInterceptState
returns kind:none when !isRscRequest, app-page-request.ts:324). The probe
fan-out called findIntercept unconditionally, so on HTML requests it would
skip the matched route's slot page (which DOES render normally) and probe
the interception page (which never renders). buildAppPageProbes now ignores
the interception match when !isRscRequest, so HTML requests probe every
slot's own page and skip the interception probe.

The source-route interception case (a different route renders) never reaches
this probe: dispatchAppPage returns the intercepted response before calling
probePage, so any interception seen here is the current-route override case.
Documented both in the buildAppPageProbes doc comment.

Addresses review feedback on #1788.

---------

Co-authored-by: James <james@eli.cx>
2026-06-06 23:47:47 +01:00
James Anderson fd086c4475 fix(app-router): apply x-edge-runtime header on all App Router response paths (#1603)
Extends #1565: the merged change gated the marker on the resolved segment
config, but only covered the two `buildAppPage*Response` builders. Five
other response construction paths bypassed those helpers and skipped the
header for edge-runtime routes:

- `app-page-cache.ts` — ISR HIT/STALE responses
- `app-server-action-execution.ts` — server action RSC responses
  (redirect, no-revalidate, re-render)
- `app-page-boundary.ts` — boundary RSC responses
- `app-page-execution.ts` — 200 + flight-payload redirect responses
- `app-page-stream.ts` — HTML stream responses used by the boundary path

Centralizes the gating in a single `applyEdgeRuntimeHeader` helper so
every site shares one source of truth for header name + condition, and
threads `isEdgeRuntime` through the fallback renderer + dispatch options
so each construction site receives it.

Adds positive + negative coverage for each newly-covered path.
2026-05-26 11:28:33 +01:00
Nathan Nguyen 3ebc28f0e1 fix: preserve Next.js cache headers for prerendered app pages (#1424)
* fix: preserve Next.js cache headers for prerendered app pages

* fix: address cache header review feedback
2026-05-22 10:08:08 +01:00
Nathan Nguyen 705da9a52f feat(cache): introduce static layout artifact reuse proof primitive (#1307)
* Enable static layout artifact reuse proof

* fix(cache): reject unproven static layout variants

Static layout reuse could previously accept public or internal candidate variant dimensions without proving that the current request had the same canonical dimensions. Public visibility only means the values are safe to expose, not that the layout artifact is invariant across routes, params, search state, or other dimensions.

Reject dimensioned static layout candidates until the API can compare candidate and current variants by canonical dimension source, name, privacy, and value hashes. Make cache outcome recording best-effort so telemetry failures cannot turn a cache hit into a miss or escape the read path.

* fix(cache): require route-budgeted layout reuse candidates

Static layout artifact reuse could still accept a raw buildCacheVariant result at the authority boundary. That allowed callers to bypass route-budget admission before receiving a canReuse true decision.

Require BuildCacheVariantWithRouteBudgetResult for createStaticLayoutArtifactReuseDecision and add a type-level regression that raw variants do not satisfy the authority input.

* chore(cache): clarify static layout proof traces

The approved review called out non-blocking ambiguity in trace fields and telemetry documentation. The proof behavior is unchanged, but the trace output and type regression are easier to reason about.

Sort unproven dimension sources defensively, rename artifact compatibility trace fallback fields, document raw app-page cache keys as internal-only, and make the raw-variant type regression explicit without a dead branch.

* chore(cache): dedupe static layout dimension sources

The unproven-dimension trace already sorted sources, but duplicate source labels could still make traces noisier than necessary. Use the existing sortedUnique helper so the source summary is deterministic and compact while dimensionCount remains exact.
2026-05-20 10:36:20 +01:00
James Anderson b4898289e7 fix(app-router): match Next.js RSC Content-Type and 404 plain-text body (#1233)
* fix(app-router): match Next.js RSC Content-Type (no charset suffix)

Next.js sets the RSC (React Server Components / 'flight') response
Content-Type to exactly 'text/x-component' (RSC_CONTENT_TYPE_HEADER in
.nextjs-ref/packages/next/src/client/components/app-router-headers.ts:17,
used by FlightRenderResult in
.nextjs-ref/packages/next/src/server/app-render/flight-render-result.ts:15).
vinext was sending 'text/x-component; charset=utf-8'. The suffix breaks
Next.js's own segment-cache deduplication checks (action-handler.ts uses
.startsWith(RSC_CONTENT_TYPE_HEADER) on the unsuffixed string) and fails
Next.js e2e tests that assert strict equality, e.g.:
  - test/e2e/app-dir/app/index.test.ts L362, L371
  - test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts L80

Replace the duplicated literal at seven sites with the existing
VINEXT_RSC_CONTENT_TYPE constant in app-rsc-cache-busting.ts.

* fix(app-router): match Next.js 404 plain-text body 'This page could not be found'

Next.js's plain-text 404 response body is exactly
'This page could not be found' (no trailing period in the response body;
the React-rendered not-found component uses the same text with a period).
vinext was returning 'Not Found' / '404 Not Found' / '404 - Not found',
which fails Next.js e2e tests that assert .toContain('This page could
not be found'), e.g.:
  - test/e2e/app-dir/app/index.test.ts (several cases)
  - test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts L52
  - test/e2e/app-dir/prefetching-not-found/prefetching-not-found.test.ts L16

Next.js source citations (in .nextjs-ref):
  - packages/next/src/server/route-modules/pages/pages-handler.ts L121, L535
  - packages/next/src/build/templates/app-route.ts L170, L349
  - packages/next/src/build/templates/app-page.ts L701, L1043
  - packages/next/src/pages/_error.tsx L7
  - packages/next/src/client/components/builtin/not-found.tsx L7 (HTML variant)

Updates the body in notFoundResponse() and the open-redirect / pages-router
SSR-fallback / worker-entry inline 404 sites. The HTTP status reason phrase
('Not Found') is unchanged. Route-specific bodies like '404 - API route not
found' are intentionally retained.
2026-05-15 12:39:05 +01:00
Nathan Nguyen 4aa7663084 fix(app-router): hard navigate stale RSC build payloads (#1178)
* fix(app-router): hard navigate stale RSC build payloads

App Router soft navigation currently accepts any valid RSC payload after deploy. That lets stale tabs decode RSC responses produced by a different build, which can break client navigation when the server and browser artifacts no longer match.

The missing invariant was that every RSC response consumed by the browser must identify the build that produced it, and the browser must reject payloads from a different build before handing them to React.

Emit a vinext-owned build ID header on App Router RSC responses, preserve it in prefetched and visited response snapshots, and hard navigate when a response is missing or mismatches the current client build ID.

* chore: fix e2e fixture hook lint suppression

* chore: avoid hook-like fixture callback name

* fix(app-router): preserve build IDs for cached RSC payloads

* fix(app-router): use public RSC compatibility IDs
2026-05-15 12:01:33 +01: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 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
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.
2026-05-06 18:37:44 +01:00
Nathan Nguyen f45fce00d5 fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings

Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses.

Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules.

Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known.

* fix(isr): address cache life review follow-ups

* chore: address latest review follow-ups

* ci: pin vite-plus setup version

* Revert "ci: pin vite-plus setup version"

This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1.

* fix: avoid blocking ISR page streams on cache metadata

* fix: preserve headers for speculative cacheLife probes

* fix: preserve prerender cacheLife metadata

* fix: preserve legacy ISR cache metadata behavior

* fix: preserve prerender seed revalidate context

* fix: harden app page cache policy metadata

* fix: resolve app router prerender conflict
2026-05-02 19:54:25 +01:00
Nathan Nguyen 1d7c031e30 feat(app-router): feed file metadata routes into head output (#891) 2026-04-30 19:38:32 +01:00
Nathan Nguyen cf5471e95a fix(app-router): fixing cache request leaks (#907) 2026-04-29 14:59:14 +01:00
Nathan Nguyen ca0d542947 Fix parallel slot persistence and cache variants (#811)
* Fix parallel slot persistence and cache variants

* Address App Router review follow-ups

* Fix generated mounted-slot regex escaping

* Address review feedback on slot normalization and cache comments

Align generated entry's __normalizeMountedSlotsHeader regex with the
typed module in app-elements.ts (both now use /\s+/). Clarify the
HTML vs RSC cache asymmetry comment in stale regen path.

* Remove redundant .map(trim) and clarify regen slot context

split(/\s+/) never produces tokens with leading/trailing whitespace,
so .map(trim) was a no-op in both normalizeMountedSlotsHeader and its
generated counterpart. Also add a comment clarifying that background
regen intentionally inherits the triggering request's slot context.

* Clarify regen dedup and prefetch slot-mismatch tradeoffs

Add comments explaining that pathname-only regen dedup is intentional
(avoids concurrent regen storms across slot variants) and that
consumePrefetchResponse unconditionally removes the entry before the
slot-mismatch check (simplest correct behavior, prefetch is wasted on
mismatch).

* Document storePrefetchResponse slot-awareness gap

Add JSDoc note that storePrefetchResponse is slot-unaware: the
snapshot's mountedSlotsHeader comes from the response headers rather
than the caller, so consumePrefetchResponse may reject the entry on
slot context mismatch.
2026-04-10 15:29:33 +01:00
Steve Faulkner 4241f5d31b fix: skip page RSC ISR caching for dynamic requests (#629)
* fix: skip page RSC ISR caching for dynamic requests

* test: align page ISR assertions with helper extraction
2026-03-21 12:19:38 -05:00
Steve Faulkner 8366217c84 refactor: extract app page cache writes (#628) 2026-03-21 11:37:46 -05:00
Steve Faulkner 32a346a1e3 refactor: extract app page cache helpers (#626)
* refactor: extract app page cache helpers

* chore: tighten app page cache extraction

* test: refresh app page cache snapshots
2026-03-21 11:03:11 -05:00