Fix dev mode reload and error on back navigation between Next.js pages#93486
Merged
Conversation
Follow-up to #92892, addressing the regression reported in #93136 (comment). That PR persisted the dev-mode debug channel chunks under a single shared `sessionStorage` key (`__next_debug_channel`) so the data could be replayed when a page is served from HTTP cache. The single-key design holds when the only navigations away from a Next.js page go to other origins, but it breaks the moment two Next.js documents share the same tab. When a user does an MPA navigation from page A to page B (a regular `<a href>` link to another Next.js route), B's persisted entry overwrites A's under the same key. Clicking back restores A from HTTP cache; A's bootstrap re-executes with its original `self.__next_r`, looks up `sessionStorage`, finds B's `requestId`, returns `undefined` from `restoreDebugChannelFromSessionStorage`, and falls back to `location.reload()`. Before the reload completes, the main RSC stream closes; the Flight client surfaces references to debug-channel chunks that block other model chunks — references the debug channel was supposed to deliver but never did — as a `Connection closed.` error, briefly flashing the error UI. The fix changes the storage key from `__next_debug_channel` to a per-document prefix `__next_debug_channel:${requestId}`. Each initial document persists into its own entry, so A's data survives B's load and the back-restore finds it. `requestId` is dropped from the stored value since it's now part of the key. On `setItem` failure (likely a quota issue), other entries with the prefix are removed and `setItem` is retried once; if it still fails, persistence is silently skipped and `location.reload()` remains the safety net on the read side. The fallback path also gets a small change: when restore legitimately fails (e.g., session storage cleared mid-session), `createDebugChannel` now returns `{ readable: new ReadableStream() }` rather than `{ readable: undefined }`. The empty stream never enqueues and never closes, so from the Flight client's perspective the debug channel is still active when the main stream closes — the unresolved chunk references no longer surface as a synchronous error before `location.reload()` tears the document down. The `bfcache-regression` test was updated to reproduce the bug. It now clicks an internal MPA link to `/target-page` instead of an external one, and asserts no console errors after the round trip via `assertNoConsoleErrors`. The fixture pre-warms `/target-page` compilation in parallel with the browser load in webpack mode, to avoid an unrelated Fast Refresh full-reload warning when the route is compiled on demand.
Contributor
Tests PassedCommit: ced9422 |
eps1lon
approved these changes
May 5, 2026
unstubbable
added a commit
that referenced
this pull request
May 6, 2026
#93486) Follow-up to #92892, addressing the regression reported in #93136 (comment). That PR persisted the dev-mode debug channel chunks under a single shared `sessionStorage` key (`__next_debug_channel`) so the data could be replayed when a page is served from HTTP cache. The single-key design holds when the only navigations away from a Next.js page go to other origins, but it breaks the moment two Next.js documents share the same tab. When a user does an MPA navigation from page A to page B (a regular `<a href>` link to another Next.js route), B's persisted entry overwrites A's under the same key. Clicking back restores A from HTTP cache; A's bootstrap re-executes with its original `self.__next_r`, looks up `sessionStorage`, finds B's `requestId`, returns `undefined` from `restoreDebugChannelFromSessionStorage`, and falls back to `location.reload()`. Before the reload completes, the main RSC stream closes; the Flight client surfaces references to debug-channel chunks that block other model chunks — references the debug channel was supposed to deliver but never did — as a `Connection closed.` error, briefly flashing the error UI. The fix changes the storage key from `__next_debug_channel` to a per-document prefix `__next_debug_channel:${requestId}`. Each initial document persists into its own entry, so A's data survives B's load and the back-restore finds it. `requestId` is dropped from the stored value since it's now part of the key. On `setItem` failure (likely a quota issue), other entries with the prefix are removed and `setItem` is retried once; if it still fails, persistence is silently skipped and `location.reload()` remains the safety net on the read side. The fallback path also gets a small change: when restore legitimately fails (e.g., session storage cleared mid-session), `createDebugChannel` now returns `{ readable: new ReadableStream() }` rather than `{ readable: undefined }`. The empty stream never enqueues and never closes, so from the Flight client's perspective the debug channel is still active when the main stream closes — the unresolved chunk references no longer surface as a synchronous error before `location.reload()` tears the document down. The `bfcache-regression` test was updated to reproduce the bug. It now clicks an internal MPA link to `/target-page` instead of an external one, and asserts no console errors after the round trip via `assertNoConsoleErrors`. The fixture pre-warms `/target-page` compilation in parallel with the browser load in webpack mode, to avoid an unrelated Fast Refresh full-reload warning when the route is compiled on demand.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #92892, addressing the regression reported in #93136 (comment).
That PR persisted the dev-mode debug channel chunks under a single shared
sessionStoragekey (__next_debug_channel) so the data could be replayed when a page is served from HTTP cache. The single-key design holds when the only navigations away from a Next.js page go to other origins, but it breaks the moment two Next.js documents share the same tab.When a user does an MPA navigation from page A to page B (a regular
<a href>link to another Next.js route), B's persisted entry overwrites A's under the same key. Clicking back restores A from HTTP cache; A's bootstrap re-executes with its originalself.__next_r, looks upsessionStorage, finds B'srequestId, returnsundefinedfromrestoreDebugChannelFromSessionStorage, and falls back tolocation.reload(). Before the reload completes, the main RSC stream closes; the Flight client surfaces references to debug-channel chunks that block other model chunks — references the debug channel was supposed to deliver but never did — as aConnection closed.error, briefly flashing the error UI.The fix changes the storage key from
__next_debug_channelto a per-document prefix__next_debug_channel:${requestId}. Each initial document persists into its own entry, so A's data survives B's load and the back-restore finds it.requestIdis dropped from the stored value since it's now part of the key. OnsetItemfailure (likely a quota issue), other entries with the prefix are removed andsetItemis retried once; if it still fails, persistence is silently skipped andlocation.reload()remains the safety net on the read side.The fallback path also gets a small change: when restore legitimately fails (e.g., session storage cleared mid-session),
createDebugChannelnow returns{ readable: new ReadableStream() }rather than{ readable: undefined }. The empty stream never enqueues and never closes, so from the Flight client's perspective the debug channel is still active when the main stream closes — the unresolved chunk references no longer surface as a synchronous error beforelocation.reload()tears the document down.The
bfcache-regressiontest was updated to reproduce the bug. It now clicks an internal MPA link to/target-pageinstead of an external one, and asserts no console errors after the round trip viaassertNoConsoleErrors. The fixture pre-warms/target-pagecompilation in parallel with the browser load in webpack mode, to avoid an unrelated Fast Refresh full-reload warning when the route is compiled on demand.Fixes: #93510