Commit Graph

5 Commits

Author SHA1 Message Date
Nathan Nguyen 8498e4a37e fix(app-router): emit per-page dynamic stale time metadata (#1711)
* fix(app-router): emit per-page dynamic stale time metadata

App Router pages that export unstable_dynamicStaleTime currently behave as if no page-level stale window exists. The server segment config resolver discards the export, so RSC responses cannot carry the per-page metadata that the client cache needs for independently configured pages.

The violated invariant is that route segment config resolution must preserve page-only stale-time metadata across active parallel pages before response construction. Parse the page export, reduce active page slots by shortest stale time, thread the value through generated App page dispatch, and emit it on RSC responses for the client cache to consume.

* fix(app-router): gate dynamic stale time header to dynamic-only renders

Only emit X-Vinext-Dynamic-Stale-Time when the render is truly dynamic
(not prerender or force-static), matching Next.js behavior where the
response `d` field is only set for non-static-generation renders.

The client treats the headers presence as authoritative; leaking it onto
static/prerendered RSC responses would cause the client cache to apply
dynamic stale time semantics to a statically-cached entry.

* test(app-router): add behavioral tests for dynamic stale time gating

* docs(app-page-render): add line-numbered link to Next.js source

* refactor(app-router): drop redundant Number.isFinite check

Number.isInteger already returns false for Infinity/NaN, so the
preceding Number.isFinite guard is redundant in both the dynamic
stale-time header check and resolveDynamicStaleTimeSeconds.

* fix(app-router): gate dynamic stale time on cache-capture predicate

The previous gating (isPrerender || isForceStatic) was too narrow for
production: isPrerender is only true during the build-time prerender
pass, so a production ISR render (revalidate > 0) still emitted
X-Vinext-Dynamic-Stale-Time even though that render is captured into the
ISR cache. Next.js gates on !workStore.isStaticGeneration; the runtime
analog here is shouldCaptureRscForCacheMetadata, so add it to the guard.

Adds production ISR coverage to lock down the gating.

* docs(app-router): document dynamic stale time over-gating boundary

Add a comment explaining that shouldCaptureRscForCacheMetadata is
computed from config before the stream runs, so a production
default-config route that turns dynamic via a late dynamic API loses the
advisory stale-time hint even though Next.js (!isStaticGeneration) would
emit it. The value is unknowable until after headers are built.

Adds a complementary test pinning down this boundary behavior.

---------

Co-authored-by: James <james@eli.cx>
2026-06-02 16:23:46 +01:00
Nathan Nguyen 8ae5abd56c fix(app-router): preserve forwarded action redirect wrappers (#1582) 2026-05-31 22:30:57 +01:00
James Anderson d0839903e8 fix(app-router): emit vary marker on edge RSC route response (#1565)
* fix(app-router): emit x-edge-runtime marker on edge RSC route response

Closes #1531

The Next.js edge-runtime app template (edge-ssr-app.ts) sets
`x-edge-runtime: 1` on every App Router response. vinext serves App
Router responses from a Cloudflare Worker (an edge-equivalent runtime)
but was not emitting this marker, so Next.js' upstream test suite
(`test/e2e/app-dir/app/index.test.ts`) saw `null` for the header.

Add the header to both the RSC and HTML response builders so the
assertion passes for any App Router page, regardless of which payload
shape the client requests.

* fix(app-router): gate x-edge-runtime marker on configured runtime

Only emit `x-edge-runtime: 1` when the route's resolved segment config
opts into the edge runtime (`export const runtime = "edge"` or
`"experimental-edge"`). Plumb the resolved runtime from the segment
config through dispatch and the render lifecycle so both RSC and HTML
response builders see it.

Without the gate, nodejs-runtime routes also advertise the marker, which
hides the configured runtime from downstream consumers.
2026-05-26 10:29:59 +01:00
Nathan Nguyen 6b7e7557a9 fix: support revalidate = false for App route segment config (#1046)
* fix: support revalidate = false for App route segment config (#1041)

- Build-time: extractExportConstNumber now matches false and returns Infinity
- Classifiers: classifyLayoutSegmentConfig and classifyAppRoute correctly
  classify revalidate = false as static (indefinite caching)
- Runtime: getAppRouteHandlerRevalidateSeconds returns Infinity for false,
  enabling ISR cache gates that emit STATIC cache-control instead of SWR
- Template: RSC entry resolves revalidate = false from page exports as Infinity
- Cache-Control: buildRouteHandlerCacheControl emits STATIC cache-control
  when revalidateSeconds is Infinity, matching the page-path behavior

Ported from Next.js segment config validation:
https://github.com/vercel/next.js/blob/canary/packages/next/src/build/segment-config/app/app-segment-config.ts

* fix: exclude Infinity from route handler ISR read/write gates

* Update packages/vinext/src/build/report.ts

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

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-04 17:04:39 +01:00
Nathan Nguyen 54a1286839 fix(app): apply layout segment config to pages (#993)
* fix(app): apply layout segment config to pages

* fix(app): address segment config review nits
2026-05-03 18:57:48 +01:00