Skip to content

fix(app-router): hard navigate stale RSC build payloads#1178

Draft
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/build-id-invalidation
Draft

fix(app-router): hard navigate stale RSC build payloads#1178
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/build-id-invalidation

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented May 12, 2026

Overview

Field Detail
Goal Prevent stale App Router tabs from applying RSC payloads produced by a different vinext build.
Core change App RSC responses now carry a vinext build ID header, and the browser hard-navigates before decoding missing or mismatched build payloads.
Main boundary The RSC response compatibility check lives with RSC URL/header helpers, while the browser entry owns when to hard navigate.
Primary files packages/vinext/src/server/app-rsc-cache-busting.ts, packages/vinext/src/server/app-browser-entry.ts, packages/vinext/src/shims/navigation.ts
Expected impact Stale tabs recover with a document navigation after deploy instead of attempting a broken soft navigation.

Why

A browser-side App Router navigation must only apply RSC data that was produced by the same server artifact as the current client bundle. Next.js enforces this by initializing a navigation build ID during hydration and falling back to an MPA navigation when a later Flight response belongs to another build. Vinext had content-type and status checks, but no build-artifact compatibility check, so stale tabs could keep using soft navigation across deploy boundaries.

Area Principle / invariant What this PR changes
RSC responses Every browser-consumed App RSC response should identify the build that produced it. Adds X-Vinext-Build-Id after middleware headers are merged so app middleware cannot override the framework value.
Browser navigation A payload from another build must not be decoded as the current app tree. Checks fetched, prefetched, visited-cache, and server-action RSC responses before createFromFetch.
Cached payload replay Cached RSC snapshots must preserve compatibility metadata. Stores and restores the build ID header alongside params and mounted-slot headers.

One PR vs split

This should be one PR. Splitting would either emit unused server metadata without protection, or add a client guard with no reliable build ID to compare. The server header, snapshot preservation, and browser guard are one observable behavior.

What changed

Scenario Before After
Fresh matching RSC response Soft navigation decoded and committed. Same behavior.
Mismatched RSC response after deploy Browser attempted to decode and render stale Flight data. Browser hard-navigates to the normalized document URL before decoding.
Missing response build ID while client has one Browser treated the payload as usable. Browser treats it as incompatible and hard-navigates.
Middleware sets X-Vinext-Build-Id Middleware could replace the framework value. Framework value is applied after middleware response headers.
Maintainer review path
  1. packages/vinext/src/server/app-rsc-cache-busting.ts: build ID header constant, compatibility predicate, and RSC hard-navigation target normalization.
  2. packages/vinext/src/server/app-browser-entry.ts: checks in server actions, visited response cache, fresh fetch, and prefetch paths before decoding RSC.
  3. packages/vinext/src/server/app-page-*.ts and app-server-action-execution.ts: all App Router RSC response constructors apply the build ID header.
  4. packages/vinext/src/shims/navigation.ts: cached/prefetched RSC snapshots preserve the build ID header.
  5. Tests listed below show the expected behavior and metadata preservation.
Validation

Ran locally:

  • vp test run tests/app-rsc-cache-busting.test.ts tests/app-page-response.test.ts tests/app-page-cache.test.ts tests/prefetch-cache.test.ts tests/app-browser-entry.test.ts
    Result: 5 files, 162 tests passed.
  • vp check
    Result: formatting passed, no warnings, lint errors, or type errors.
  • Commit hook also ran formatting, lint/type checks, and knip --no-progress.

Review feedback addressed:

  • Missing response build IDs are incompatible when the client has a build ID.
  • Middleware cannot override the framework build ID header.
  • CachedRscResponse declaration now matches runtime shape.
  • RSC mounted-slot header snapshotting now uses the shared constant.
Risk / compatibility
  • Runtime: affects App Router browser RSC navigation and server-action re-render responses.
  • Cache: existing cached RSC payloads without the new header hard-navigate when the client has a build ID. That is intentional because missing metadata is not safe to decode after this change.
  • Public API: no intended public API change. The ambient shim declaration is updated to match the internal cached response shape.
  • Middleware: app middleware can still set user headers, but the vinext build ID header remains framework-owned.
Non-goals
  • Does not add Next.js deployment ID multi-zone semantics. This only compares vinext build IDs.
  • Does not change Pages Router data route build ID behavior.
  • Does not redesign the App Router browser entry or RSC payload envelope.

References

Reference Why it matters
Next.js navigation build ID state Stores the client-side build/deployment ID used for navigation compatibility checks.
Next.js hydration initializes navigation build ID Sets the navigation build ID from the initial RSC payload or deployment ID before hydration.
Next.js fetchServerResponse build check Falls back to MPA navigation when a Flight response build ID does not match the client build ID.
Next.js app render attaches build ID to initial RSC payload Shows the server side source of the build ID metadata used by the browser.

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.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1178

commit: 101b5cc

@NathanDrake2406 NathanDrake2406 marked this pull request as draft May 12, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant