Skip to content

fix: trigger MPA navigation for server action redirects with build ID mismatch#89946

Merged
ztanner merged 6 commits intocanaryfrom
fix/cross-zone-server-action-redirect
Mar 6, 2026
Merged

fix: trigger MPA navigation for server action redirects with build ID mismatch#89946
ztanner merged 6 commits intocanaryfrom
fix/cross-zone-server-action-redirect

Conversation

@jonasherr
Copy link
Contributor

@jonasherr jonasherr commented Feb 13, 2026

Summary

  • Fixes cross-zone server action redirect failure where redirect() inside a server action produces a blank page when the target path is served by a different Next.js zone (separate Vercel project, same domain)
  • Adds build ID validation to server-action-reducer.ts, matching the existing pattern in fetch-server-response.ts
  • Adds e2e test in the deployment-skew test suite to verify server action redirects with build ID mismatch trigger MPA navigation

Background

When a server action calls redirect(), the server pre-fetches the redirect target and includes the RSC data in the response. In a multi-zone setup (same domain, different Next.js projects), this pre-fetched RSC data comes from a different build with a different build ID.

The fetch-server-response.ts file already checks for build ID mismatches during regular navigations (line 245-251) and triggers MPA navigation when detected. However, server-action-reducer.ts was missing this check, causing the client to try to apply the foreign RSC payload — resulting in a blank page.

Fix

Added a build ID check in fetchServerAction() after parsing the RSC response. When a mismatch is detected, the flight data is discarded. The existing reducer logic at line 364-370 already handles this case:

if (flightData === undefined && redirectLocation !== undefined) {
  return completeHardNavigation(state, redirectLocation, navigateType)
}

This triggers an MPA navigation (full page load), which is the correct behavior for cross-zone redirects.

Repro

Closes https://linear.app/vercel/issue/NEXT-4856

@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: c0e1c5c

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 13, 2026

Allow CI Workflow Run

  • approve CI run for commit: ec2c7f9

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@jonasherr jonasherr requested review from acdlite and gnoff February 13, 2026 08:57
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 20, 2026

Tests Passed

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 20, 2026

Stats from current PR

🟢 1 improvement

Metric Canary PR Change Trend
node_modules Size 477 MB 476 MB 🟢 1.06 MB (0%) ███▁█
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 558ms 559ms ▁▁▁██
Cold (Ready in log) 521ms 523ms ▂▂▁██
Cold (First Request) 1.116s 1.130s ▄▄▁██
Warm (Listen) 558ms 557ms ▁▁▁██
Warm (Ready in log) 516ms 517ms ▁▁▁██
Warm (First Request) 400ms 408ms ▁▁▁▇█
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms █▁█▁█
Cold (Ready in log) 438ms 439ms ▁▆▇██
Cold (First Request) 1.942s 1.940s ▁▄█▅█
Warm (Listen) 457ms 456ms █████
Warm (Ready in log) 437ms 439ms ▂▇▇▇█
Warm (First Request) 1.941s 1.965s ▂▅█▅█

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 4.823s 4.926s ▁▁▁██
Cached Build 4.815s 4.921s ▁▁▁█▇
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.060s 14.131s ▁▂█▂█
Cached Build 14.241s 14.349s ▁▂█▂█
node_modules Size 477 MB 476 MB 🟢 1.06 MB (0%) ███▁█
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **402 kB** → **401 kB** ✅ -745 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 764 B 763 B
Total 764 B 763 B ✅ -1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 451 B 452 B
Total 451 B 452 B ⚠️ +1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 59.5 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 253 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 59.5 kB -
Total 233 kB 233 kB ✅ -34 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.5 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.97 kB ✅ -14 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 256 kB 255 kB
Total 381 kB 380 kB ✅ -1.1 kB
Middleware
Canary PR Change
middleware-b..fest.js gzip 617 B 614 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.8 kB 43.7 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.4 kB 45.3 kB ✅ -106 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.07 MB 4.06 MB 🟢 15.5 kB (0%)
index.pack gzip 104 kB 102 kB 🟢 1.32 kB (-1%)
index.pack.old gzip 103 kB 103 kB
Total 4.28 MB 4.26 MB ✅ -16.8 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 322 kB 321 kB
app-page-exp..prod.js gzip 171 kB 170 kB
app-page-tur...dev.js gzip 322 kB 320 kB
app-page-tur..prod.js gzip 171 kB 170 kB
app-page-tur...dev.js gzip 319 kB 317 kB
app-page-tur..prod.js gzip 169 kB 168 kB
app-page.run...dev.js gzip 319 kB 317 kB
app-page.run..prod.js gzip 169 kB 168 kB
app-route-ex...dev.js gzip 70.9 kB 70.8 kB
app-route-ex..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.9 kB 70.9 kB
app-route-tu..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.5 kB 70.5 kB
app-route-tu..prod.js gzip 49.1 kB 49 kB
app-route.ru...dev.js gzip 70.5 kB 70.4 kB
app-route.ru..prod.js gzip 49 kB 49 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.3 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.9 kB 32.9 kB
pages-turbo....dev.js gzip 52.6 kB 52.6 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.6 kB 52.6 kB
pages.runtim..prod.js gzip 38.5 kB 38.5 kB
server.runti..prod.js gzip 62 kB 62 kB
Total 2.84 MB 2.83 MB ✅ -11.6 kB
📝 Changed Files (25 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • app-route-ex..ntime.dev.js
  • app-route-ex..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route.runtime.dev.js
  • app-route.ru..time.prod.js
  • pages-api-tu..ntime.dev.js
  • pages-api-tu..time.prod.js
  • pages-api.runtime.dev.js
  • pages-api.ru..time.prod.js
  • ... and 5 more
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js
failed to diff
app-route-ex..ntime.dev.js

Diff too large to display

app-route-ex..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route.runtime.dev.js

Diff too large to display

app-route.ru..time.prod.js

Diff too large to display

pages-api-tu..ntime.dev.js

Diff too large to display

pages-api-tu..time.prod.js

Diff too large to display

pages-api.runtime.dev.js

Diff too large to display

pages-api.ru..time.prod.js

Diff too large to display

pages-turbo...ntime.dev.js

Diff too large to display

pages-turbo...time.prod.js

Diff too large to display

pages.runtime.dev.js

Diff too large to display

pages.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/10cec54048bbb6343aff3cc203dd997b0d91a13b/next

@ztanner ztanner added the CI approved Approve running CI for fork label Feb 20, 2026
@jonasherr jonasherr force-pushed the fix/cross-zone-server-action-redirect branch 3 times, most recently from 4198d47 to 2f45f98 Compare February 20, 2026 20:15
… mismatch

When a server action calls redirect() and the redirect target is served by
a different Next.js zone (separate Vercel project on same domain), the server
pre-fetches the redirect target as RSC. In a multi-zone setup, this RSC
response comes from a different build with a different build ID.

Previously, the server-action-reducer did not check the build ID of the
action response before applying the RSC payload. This caused a blank page
because the client tried to use RSC data from a foreign build.

This fix adds a build ID check to fetchServerAction(), matching the pattern
already used in fetch-server-response.ts for regular navigations. When a
build ID mismatch is detected, the flight data is discarded and the redirect
falls through to completeHardNavigation(), which triggers an MPA navigation
(full page load) — the correct behavior for cross-zone redirects.
Extends the existing deployment-skew test to verify that when a server action
calls redirect() and the target is served by a different deployment (different
build ID), the client performs an MPA navigation instead of a client-side
transition.
…ment ID handling

- Hoist actionResult and couldBeIntercepted assignments above the build ID
  check since they're identical in both branches
- Update the comment in app-page.ts that incorrectly stated 'the client
  doesn't check the build ID for action responses' — it now does. The
  exclusion of server actions from the deployment ID header is still correct
  because action redirect responses get the header from the pre-fetched
  redirect target (via createRedirectRenderResult in action-handler.ts).
Two issues fixed:

1. Set __NEXT_TEST_MODE in custom builds so the __NEXT_HYDRATED callback
   is included. Without this, webdriver() hydration detection times out
   and the test clicks the button before React hydrates, causing
   'Router action dispatched before initialization'.

2. Inject a foreign x-nextjs-deployment-id header on POST responses via
   the proxy's proxyRes handler. This simulates a CDN reporting a
   different deployment ID, triggering client-side mismatch detection
   which discards flight data and falls back to MPA navigation.
@jonasherr jonasherr force-pushed the fix/cross-zone-server-action-redirect branch from 3c32760 to 991ede0 Compare March 2, 2026 13:15
@jonasherr jonasherr requested a review from ztanner March 4, 2026 07:08
@ztanner ztanner merged commit c7df8e9 into canary Mar 6, 2026
284 of 286 checks passed
@ztanner ztanner deleted the fix/cross-zone-server-action-redirect branch March 6, 2026 22:17
sokra pushed a commit that referenced this pull request Mar 6, 2026
… mismatch (#89946)

## Summary

- Fixes cross-zone server action redirect failure where `redirect()`
inside a server action produces a blank page when the target path is
served by a different Next.js zone (separate Vercel project, same
domain)
- Adds build ID validation to `server-action-reducer.ts`, matching the
existing pattern in `fetch-server-response.ts`
- Adds e2e test in the deployment-skew test suite to verify server
action redirects with build ID mismatch trigger MPA navigation

## Background

When a server action calls `redirect()`, the server pre-fetches the
redirect target and includes the RSC data in the response. In a
multi-zone setup (same domain, different Next.js projects), this
pre-fetched RSC data comes from a different build with a different build
ID.

The `fetch-server-response.ts` file already checks for build ID
mismatches during regular navigations (line 245-251) and triggers MPA
navigation when detected. However, `server-action-reducer.ts` was
missing this check, causing the client to try to apply the foreign RSC
payload — resulting in a blank page.

## Fix

Added a build ID check in `fetchServerAction()` after parsing the RSC
response. When a mismatch is detected, the flight data is discarded. The
existing reducer logic at line 364-370 already handles this case:

```typescript
if (flightData === undefined && redirectLocation !== undefined) {
  return completeHardNavigation(state, redirectLocation, navigateType)
}
```

This triggers an MPA navigation (full page load), which is the correct
behavior for cross-zone redirects.

## Repro

- https://repro-main-app.vercel.app/test-redirect
- Red button (server action redirect) → blank page (before fix) / full
page load (after fix)
  - Green link (normal `<a>`) → works fine

Closes https://linear.app/vercel/issue/NEXT-4856

---------

Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI approved Approve running CI for fork tests type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants