nextjs - ✅(Solved) Fix App Router: Flight decode can execute incompatible modules before build mismatch recovery during rolling deploy [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#90622Fetched 2026-04-08 00:19:44
View on GitHub
Comments
1
Participants
2
Timeline
11
Reactions
9
Author
Participants
Timeline (top)
subscribed ×3comment_deleted ×2cross-referenced ×2labeled ×2

Fix Action

Fix / Workaround

Current behavior (unpatched): During rolling-deploy skew (Build A runtime in browser, Build B RSC/navigation response from server), the client can execute incompatible modules/chunks before mismatch recovery and hit fatal runtime errors (TypeError / ChunkLoadError).

  • ./scripts/reproduce.sh (unpatched): demonstrates fatal runtime break in browser before safe recovery.
  • ./scripts/reproduce-fixed.sh (patched demo): same scenario, but with an early header check that forces hard reload before decode executes incompatible modules.

This is why the patched demo logs:

PR fix notes

PR #90657: fix: detect build mismatch before Flight decode to prevent version skew crashes

Description (problem / solution / changelog)

Summary

Fixes #90622

During rolling deploys (Build A runtime in browser, Build B RSC response from server), the Flight client starts decoding incompatible modules before the build mismatch check runs, causing fatal runtime errors (TypeError: ... is not a function, ChunkLoadError).

Root Cause

In fetch-server-response.ts, createFromNextFetch() is called with the raw fetch promise immediately, which starts Flight decode in the background. The build ID comparison only happens after await flightResponsePromise completes — by which point React has already evaluated incompatible module exports from the wrong build.

Changes

Server-side (app-page.ts):

  • Always send x-nextjs-deployment-id response header for RSC navigation requests, using deploymentId || buildId
  • Previously this header was only sent when deploymentId was explicitly configured (skew protection). Now the build identity is always available in the header, enabling early client-side detection.

Client-side (fetch-server-response.ts):

  • Wrap the fetch promise with a build ID header check before passing it to createFromNextFetch. On mismatch, the response body is cancelled and the promise rejects — preventing Flight from ever decoding incompatible modules.
  • Add an early header-based mismatch check in fetchServerResponse() that triggers MPA navigation before awaiting the Flight response promise.
  • Apply the same guard to the redirect replay loop.

Flow After Fix

fetch resolves
  → buildCheckedFetchPromise intercepts response
  → reads x-nextjs-deployment-id header
  → if mismatch: cancel body + reject → Flight decode never starts
  → fetchServerResponse sees header mismatch → doMpaNavigation() (hard reload)
  → if match: pass response through → zero overhead

Test Plan

  • Verify rolling deploy scenario: deploy Build A, navigate, deploy Build B, perform client-side navigation — should hard reload instead of crashing
  • Verify normal navigation (no version skew) works unchanged
  • Verify deploymentId (skew protection) path still works correctly
  • Verify non-deploymentId path (build ID in .b field) still works as fallback
  • Verify redirect replay loop handles build mismatch correctly

Changed files

  • AGENTS.md (modified, +4/-4)
  • packages/next/src/build/templates/app-page.ts (modified, +12/-7)
  • packages/next/src/client/components/router-reducer/fetch-server-response.ts (modified, +41/-2)

Code Example

Operating System: macOS (darwin 25.3.0)
Node.js: 20+
next: 16.1.5
react: 19.x
react-dom: 19.x
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/wa3l/nextjs-version-skew-repro

To Reproduce

  1. Clone the repro repo.
  2. Install dependencies.
  3. Run: ./scripts/reproduce.sh
  4. Open http://localhost:3000 immediately.
  5. In DevTools Console, enable Preserve log. <img width="828" height="182" alt="Image" src="https://github.com/user-attachments/assets/3d068556-9989-4b48-a440-d3f9a4c49877" />
  6. Wait for the deploy message (~10s).
  7. Click Other Page. Observed in console (example):
  • Uncaught TypeError: (0 , n.tripleCount) is not a function
  • thrown at module evaluation in a client chunk
  • in some runs/environments: ChunkLoadError

Current vs. Expected behavior

Current behavior (unpatched): During rolling-deploy skew (Build A runtime in browser, Build B RSC/navigation response from server), the client can execute incompatible modules/chunks before mismatch recovery and hit fatal runtime errors (TypeError / ChunkLoadError).

Expected behavior: Build mismatch should be detected before Flight decode/evaluation of incompatible modules, so the client can hard reload first and avoid fatal runtime breakage.

Provide environment information

Operating System: macOS (darwin 25.3.0)
Node.js: 20+
next: 16.1.5
react: 19.x
react-dom: 19.x

Which area(s) are affected? (Select all that apply)

Module Resolution

Which stage(s) are affected? (Select all that apply)

Other (Deployed)

Additional context

This appears to be an ordering problem in fetch-server-response during rolling deploy skew.

What skew means in this report

  • Browser has already booted with Build A JS runtime and module registry in memory.
  • A client-side navigation request is routed to a server now running Build B.
  • The RSC/Flight payload references Build B modules/chunks.

Why this fails today

In fetch-server-response, decode can start in createFetch via createFromNextFetch(...) before the build mismatch check (flightResponse.b) is evaluated later in fetchServerResponse.

So there is a window where Build B module references are being decoded/resolved/evaluated inside a Build A-initialized runtime. That can produce:

  • missing or incompatible export shape (TypeError: ... is not a function)
  • chunk failures (ChunkLoadError) depending on asset mapping and cache/CDN behavior
  • other runtime binding/context mismatches

What this repro intentionally proves

The repro intentionally creates deterministic A->B skew and forces one incompatible module path:

  • ./scripts/reproduce.sh (unpatched): demonstrates fatal runtime break in browser before safe recovery.
  • ./scripts/reproduce-fixed.sh (patched demo): same scenario, but with an early header check that forces hard reload before decode executes incompatible modules.

This is why the patched demo logs:

  • [version-skew-fix] Early build mismatch detected; forcing hard reload

and avoids the fatal tripleCount is not a function crash.

Proposed fix direction

Make build identity available before decode (e.g. x-build-id response header) and check it before creating/awaiting Flight decode for navigation responses.

If mismatch:

  • skip decode path
  • trigger hard navigation/reload immediately

extent analysis

Fix: Early Build‑ID Check → Hard‑Reload on Version Skew

The crash happens because the client starts decoding the Flight payload before it knows that the server is running a different build.
The fix is to expose the current build‑id in a response header (e.g. x-next-build-id) and have the client read that header first. If the header value differs from the build‑id that was used to bootstrap the browser, abort the Flight decode and force a full reload.

1. Server – expose the build‑id

// src/pages/_middleware.ts  (or app router middleware)
import { NextResponse } from 'next/server';
import { getBuildId } from 'next/dist/server/config-shared';

export async function middleware(req) {
  const res = NextResponse.next();

  // getBuildId reads .next/BUILD_ID (generated at build time)
  const buildId = await getBuildId();          // e.g. "20240316-abcdef"
  res.headers.set('x-next-build-id', buildId);

  return res;
}

If you use the Edge Runtime, the same code works – just import from next/server.

2. Client – read the header before Flight decode

Patch fetch-server-response (or the wrapper you use for next/navigation) to:

import

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING