Skip to content

Fix dev mode reload and error on back navigation between Next.js pages#93486

Merged
unstubbable merged 1 commit into
canaryfrom
hl/follow-up-92892
May 5, 2026
Merged

Fix dev mode reload and error on back navigation between Next.js pages#93486
unstubbable merged 1 commit into
canaryfrom
hl/follow-up-92892

Conversation

@unstubbable

@unstubbable unstubbable commented May 5, 2026

Copy link
Copy Markdown
Contributor

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.

Fixes: #93510

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.
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: ced9422

@unstubbable unstubbable marked this pull request as ready for review May 5, 2026 10:54
@unstubbable unstubbable requested a review from eps1lon May 5, 2026 10:54
@unstubbable unstubbable merged commit c8c9fc6 into canary May 5, 2026
184 of 185 checks passed
@unstubbable unstubbable deleted the hl/follow-up-92892 branch May 5, 2026 12:24
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.
unstubbable added a commit that referenced this pull request May 19, 2026
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators May 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router dev mode: browser Back after notFound() leaves previous page detached (silent break on stable, Connection closed + auto-reload on canary)

2 participants