From a1986c81c0a62e06af13cb85433495f7d517769b Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Wed, 5 Nov 2025 12:01:54 +0100 Subject: [PATCH 1/2] Resolve request ID confusion Setting the cache status for a page in dev mode is a sequential operation that only requires the HTML request ID to identify WebSocket clients across different browser tabs/windows. This is different from connecting the React debug channel (after which the cache status handling was modeled), which requires both the HTML request ID as well as the request ID to correctly associate debug chunks for a given client with the matching request, which might be for the HTML document, a client-side navigation, or a server function call. With this PR, we're removing the unused `requestId` parameter from the `setCacheStatus` function. We're also renaming the WebSocket client's request ID to `htmlRequestId`, as well as renaming the associated maps and variables accordingly. This avoids confusion and better reflects the purpose of these IDs. One of those confusions was apparent in the `setReactDebugChannel` methods, which is now cleared up as well. --- .../next/src/server/app-render/app-render.tsx | 11 +- packages/next/src/server/app-render/types.ts | 6 +- packages/next/src/server/dev/debug-channel.ts | 38 ++++-- .../next/src/server/dev/hot-middleware.ts | 55 ++++---- .../src/server/dev/hot-reloader-turbopack.ts | 122 ++++++++++-------- .../next/src/server/dev/hot-reloader-types.ts | 6 +- .../src/server/dev/hot-reloader-webpack.ts | 63 +++++---- .../src/server/lib/dev-bundler-service.ts | 5 +- .../lib/router-utils/router-server-context.ts | 6 +- 9 files changed, 175 insertions(+), 137 deletions(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index b1b890115ced1a..9a8c32533ad6f1 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -792,7 +792,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev( // Before we kick off the render, we set the cache status back to it's initial state // in case a previous render bypassed the cache. if (setCacheStatus) { - setCacheStatus('ready', htmlRequestId, requestId) + setCacheStatus('ready', htmlRequestId) } const result = await renderWithRestartOnCacheMissInDev( @@ -812,7 +812,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev( // Set cache status to bypass when specifically bypassing caches in dev if (setCacheStatus) { - setCacheStatus('bypass', htmlRequestId, requestId) + setCacheStatus('bypass', htmlRequestId) } debugChannel = setReactDebugChannel && createDebugChannel() @@ -2613,7 +2613,7 @@ async function renderToStream( // This lets the client know not to cache anything based on this render. if (renderOpts.setCacheStatus) { // we know this is available when cacheComponents is enabled, but typeguard to be safe - renderOpts.setCacheStatus('bypass', htmlRequestId, requestId) + renderOpts.setCacheStatus('bypass', htmlRequestId) } payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, { route: workStore.route, @@ -3060,7 +3060,6 @@ async function renderWithRestartOnCacheMissInDev( const { htmlRequestId, renderOpts, - requestId, componentMod: { routeModule: { userland: { loaderTree }, @@ -3203,7 +3202,7 @@ async function renderWithRestartOnCacheMissInDev( } if (process.env.NODE_ENV === 'development' && setCacheStatus) { - setCacheStatus('filling', htmlRequestId, requestId) + setCacheStatus('filling', htmlRequestId) } // Cache miss. We will use the initial render to fill caches, and discard its result. @@ -3276,7 +3275,7 @@ async function renderWithRestartOnCacheMissInDev( ) if (process.env.NODE_ENV === 'development' && setCacheStatus) { - setCacheStatus('filled', htmlRequestId, requestId) + setCacheStatus('filled', htmlRequestId) } return { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 3435a58415c6c6..8eb6a5a75d775f 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -109,11 +109,7 @@ export interface RenderOptsPartial { } isOnDemandRevalidate?: boolean isPossibleServerAction?: boolean - setCacheStatus?: ( - status: ServerCacheStatus, - htmlRequestId: string, - requestId: string - ) => void + setCacheStatus?: (status: ServerCacheStatus, htmlRequestId: string) => void setIsrStatus?: (key: string, value: boolean | undefined) => void setReactDebugChannel?: ( debugChannel: { readable: ReadableStream }, diff --git a/packages/next/src/server/dev/debug-channel.ts b/packages/next/src/server/dev/debug-channel.ts index ad202df7e62803..a0a052b8426a1d 100644 --- a/packages/next/src/server/dev/debug-channel.ts +++ b/packages/next/src/server/dev/debug-channel.ts @@ -9,21 +9,16 @@ export interface ReactDebugChannelForBrowser { // Might also get a writable stream as return channel in the future. } -const reactDebugChannelsByRequestId = new Map< +const reactDebugChannelsByHtmlRequestId = new Map< string, ReactDebugChannelForBrowser >() export function connectReactDebugChannel( requestId: string, + debugChannel: ReactDebugChannelForBrowser, sendToClient: (message: HmrMessageSentToBrowser) => void ) { - const debugChannel = reactDebugChannelsByRequestId.get(requestId) - - if (!debugChannel) { - return - } - const reader = debugChannel.readable .pipeThrough( // We're sending the chunks in batches to reduce overhead in the browser. @@ -37,8 +32,6 @@ export function connectReactDebugChannel( requestId, chunk: null, }) - - reactDebugChannelsByRequestId.delete(requestId) } const onError = (err: unknown) => { @@ -63,13 +56,30 @@ export function connectReactDebugChannel( reader.read().then(progress, onError) } -export function setReactDebugChannel( - requestId: string, +export function connectReactDebugChannelForHtmlRequest( + htmlRequestId: string, + sendToClient: (message: HmrMessageSentToBrowser) => void +) { + const debugChannel = reactDebugChannelsByHtmlRequestId.get(htmlRequestId) + + if (!debugChannel) { + return + } + + reactDebugChannelsByHtmlRequestId.delete(htmlRequestId) + + connectReactDebugChannel(htmlRequestId, debugChannel, sendToClient) +} + +export function setReactDebugChannelForHtmlRequest( + htmlRequestId: string, debugChannel: ReactDebugChannelForBrowser ) { - reactDebugChannelsByRequestId.set(requestId, debugChannel) + // TODO: Clean up after a timeout, in case the client never connects, e.g. + // when CURL'ing the page, or loading the page with JavaScript disabled etc. + reactDebugChannelsByHtmlRequestId.set(htmlRequestId, debugChannel) } -export function deleteReactDebugChannel(requestId: string) { - reactDebugChannelsByRequestId.delete(requestId) +export function deleteReactDebugChannelForHtmlRequest(htmlRequestId: string) { + reactDebugChannelsByHtmlRequestId.delete(htmlRequestId) } diff --git a/packages/next/src/server/dev/hot-middleware.ts b/packages/next/src/server/dev/hot-middleware.ts index 7cadd33619820b..3a7e0623262b27 100644 --- a/packages/next/src/server/dev/hot-middleware.ts +++ b/packages/next/src/server/dev/hot-middleware.ts @@ -71,8 +71,8 @@ function getStatsForSyncEvent( } export class WebpackHotMiddleware { - private clientsWithoutRequestId = new Set() - private clientsByRequestId: Map = new Map() + private clientsWithoutHtmlRequestId = new Set() + private clientsByHtmlRequestId: Map = new Map() private closed = false private clientLatestStats: { ts: number; stats: webpack.Stats } | null = null private middlewareLatestStats: { ts: number; stats: webpack.Stats } | null = @@ -163,20 +163,20 @@ export class WebpackHotMiddleware { * and we still want to show the client overlay with the error while * the error page should be rendered just fine. */ - onHMR = (client: ws, requestId: string | null) => { + onHMR = (client: ws, htmlRequestId: string | null) => { if (this.closed) return - if (requestId) { - this.clientsByRequestId.set(requestId, client) + if (htmlRequestId) { + this.clientsByHtmlRequestId.set(htmlRequestId, client) } else { - this.clientsWithoutRequestId.add(client) + this.clientsWithoutHtmlRequestId.add(client) } client.addEventListener('close', () => { - if (requestId) { - this.clientsByRequestId.delete(requestId) + if (htmlRequestId) { + this.clientsByHtmlRequestId.delete(htmlRequestId) } else { - this.clientsWithoutRequestId.delete(client) + this.clientsWithoutHtmlRequestId.delete(client) } }) @@ -228,8 +228,8 @@ export class WebpackHotMiddleware { }) } - getClient = (requestId: string): ws | undefined => { - return this.clientsByRequestId.get(requestId) + getClient = (htmlRequestId: string): ws | undefined => { + return this.clientsByHtmlRequestId.get(htmlRequestId) } publishToClient = (client: ws, message: HmrMessageSentToBrowser) => { @@ -251,8 +251,8 @@ export class WebpackHotMiddleware { } for (const wsClient of [ - ...this.clientsWithoutRequestId, - ...this.clientsByRequestId.values(), + ...this.clientsWithoutHtmlRequestId, + ...this.clientsByHtmlRequestId.values(), ]) { this.publishToClient(wsClient, message) } @@ -270,12 +270,12 @@ export class WebpackHotMiddleware { // inferring it from the presence of a request ID. if (!this.config.cacheComponents) { - for (const wsClient of this.clientsByRequestId.values()) { + for (const wsClient of this.clientsByHtmlRequestId.values()) { this.publishToClient(wsClient, message) } } - for (const wsClient of this.clientsWithoutRequestId) { + for (const wsClient of this.clientsWithoutHtmlRequestId) { this.publishToClient(wsClient, message) } } @@ -290,30 +290,35 @@ export class WebpackHotMiddleware { this.closed = true for (const wsClient of [ - ...this.clientsWithoutRequestId, - ...this.clientsByRequestId.values(), + ...this.clientsWithoutHtmlRequestId, + ...this.clientsByHtmlRequestId.values(), ]) { // it's okay to not cleanly close these websocket connections, this is dev wsClient.terminate() } - this.clientsWithoutRequestId.clear() - this.clientsByRequestId.clear() + this.clientsWithoutHtmlRequestId.clear() + this.clientsByHtmlRequestId.clear() } - deleteClient = (client: ws, requestId: string | null) => { - if (requestId) { - this.clientsByRequestId.delete(requestId) + deleteClient = (client: ws, htmlRequestId: string | null) => { + if (htmlRequestId) { + this.clientsByHtmlRequestId.delete(htmlRequestId) } else { - this.clientsWithoutRequestId.delete(client) + this.clientsWithoutHtmlRequestId.delete(client) } } hasClients = () => { - return this.clientsWithoutRequestId.size + this.clientsByRequestId.size > 0 + return ( + this.clientsWithoutHtmlRequestId.size + this.clientsByHtmlRequestId.size > + 0 + ) } getClientCount = () => { - return this.clientsWithoutRequestId.size + this.clientsByRequestId.size + return ( + this.clientsWithoutHtmlRequestId.size + this.clientsByHtmlRequestId.size + ) } } diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 7036ff0c88bb87..82b8b18e1385fc 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -109,8 +109,9 @@ import { } from '../../next-devtools/server/devtools-config-middleware' import { connectReactDebugChannel, - deleteReactDebugChannel, - setReactDebugChannel, + connectReactDebugChannelForHtmlRequest, + deleteReactDebugChannelForHtmlRequest, + setReactDebugChannelForHtmlRequest, } from './debug-channel' import { getVersionInfo, @@ -439,9 +440,9 @@ export async function createHotReloaderTurbopack( let hmrEventHappened = false let hmrHash = 0 - const clientsWithoutRequestId = new Set() - const clientsByRequestId = new Map() - const cacheStatusesByRequestId = new Map() + const clientsWithoutHtmlRequestId = new Set() + const clientsByHtmlRequestId = new Map() + const cacheStatusesByHtmlRequestId = new Map() const clientStates = new WeakMap() function sendToClient(client: ws, message: HmrMessageSentToBrowser) { @@ -465,8 +466,8 @@ export async function createHotReloaderTurbopack( } for (const client of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { const state = clientStates.get(client) if (!state) { @@ -501,8 +502,8 @@ export async function createHotReloaderTurbopack( const sendHmr: SendHmr = (id: string, message: HmrMessageSentToBrowser) => { for (const client of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { clientStates.get(client)?.messages.set(id, message) } @@ -519,8 +520,8 @@ export async function createHotReloaderTurbopack( payload.issues = [] for (const client of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { clientStates.get(client)?.turbopackUpdates.push(payload) } @@ -683,7 +684,10 @@ export async function createHotReloaderTurbopack( dev: { assetMapper, changeSubscriptions, - clients: [...clientsWithoutRequestId, ...clientsByRequestId.values()], + clients: [ + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), + ], clientStates, serverFields, @@ -779,7 +783,7 @@ export async function createHotReloaderTurbopack( distDir, sendHmrMessage: (message) => hotReloader.send(message), getActiveConnectionCount: () => - clientsWithoutRequestId.size + clientsByRequestId.size, + clientsWithoutHtmlRequestId.size + clientsByHtmlRequestId.size, getDevServerUrl: () => process.env.__NEXT_PRIVATE_ORIGIN, }), ] @@ -877,7 +881,7 @@ export async function createHotReloaderTurbopack( const clientIssues: EntryIssuesMap = new Map() const subscriptions: Map> = new Map() - const requestId = req.url + const htmlRequestId = req.url ? new URL(req.url, 'http://n').searchParams.get('id') : null @@ -886,24 +890,24 @@ export async function createHotReloaderTurbopack( // Router clients are also considered legacy clients. TODO: Maybe mark // clients as App Router / Pages Router clients explicitly, instead of // inferring it from the presence of a request ID. - if (requestId) { - clientsByRequestId.set(requestId, client) + if (htmlRequestId) { + clientsByHtmlRequestId.set(htmlRequestId, client) const enableCacheComponents = nextConfig.cacheComponents if (enableCacheComponents) { onUpgrade(client, { isLegacyClient: false }) - const cacheStatus = cacheStatusesByRequestId.get(requestId) + const cacheStatus = cacheStatusesByHtmlRequestId.get(htmlRequestId) if (cacheStatus !== undefined) { sendToClient(client, { type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR, state: cacheStatus, }) - cacheStatusesByRequestId.delete(requestId) + cacheStatusesByHtmlRequestId.delete(htmlRequestId) } } else { onUpgrade(client, { isLegacyClient: true }) } } else { - clientsWithoutRequestId.add(client) + clientsWithoutHtmlRequestId.add(client) onUpgrade(client, { isLegacyClient: true }) } @@ -921,11 +925,11 @@ export async function createHotReloaderTurbopack( } clientStates.delete(client) - if (requestId) { - clientsByRequestId.delete(requestId) - deleteReactDebugChannel(requestId) + if (htmlRequestId) { + clientsByHtmlRequestId.delete(htmlRequestId) + deleteReactDebugChannelForHtmlRequest(htmlRequestId) } else { - clientsWithoutRequestId.delete(client) + clientsWithoutHtmlRequestId.delete(client) } }) @@ -1094,8 +1098,11 @@ export async function createHotReloaderTurbopack( sendToClient(client, syncMessage) - if (requestId) { - connectReactDebugChannel(requestId, sendToClient.bind(null, client)) + if (htmlRequestId) { + connectReactDebugChannelForHtmlRequest( + htmlRequestId, + sendToClient.bind(null, client) + ) } })() }) @@ -1105,8 +1112,8 @@ export async function createHotReloaderTurbopack( const payload = JSON.stringify(action) for (const client of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { client.send(payload) } @@ -1122,23 +1129,19 @@ export async function createHotReloaderTurbopack( // inferring it from the presence of a request ID. if (!nextConfig.cacheComponents) { - for (const client of clientsByRequestId.values()) { + for (const client of clientsByHtmlRequestId.values()) { client.send(payload) } } - for (const client of clientsWithoutRequestId) { + for (const client of clientsWithoutHtmlRequestId) { client.send(payload) } }, - setCacheStatus( - status: ServerCacheStatus, - htmlRequestId: string, - requestId: string - ): void { + setCacheStatus(status: ServerCacheStatus, htmlRequestId: string): void { // Legacy clients don't have Cache Components. - const client = clientsByRequestId.get(htmlRequestId) + const client = clientsByHtmlRequestId.get(htmlRequestId) if (client !== undefined) { sendToClient(client, { type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR, @@ -1147,20 +1150,37 @@ export async function createHotReloaderTurbopack( } else { // If the client is not connected, store the status so that we can send it // when the client connects. - cacheStatusesByRequestId.set(requestId, status) + cacheStatusesByHtmlRequestId.set(htmlRequestId, status) } }, setReactDebugChannel(debugChannel, htmlRequestId, requestId) { - // Store the debug channel, regardless of whether the client is connected. - setReactDebugChannel(requestId, debugChannel) - - // If the client is connected, we can connect the debug channel - // immediately. Otherwise, we'll do that when the client connects. - const client = clientsByRequestId.get(htmlRequestId) - - if (client) { - connectReactDebugChannel(requestId, sendToClient.bind(null, client)) + const client = clientsByHtmlRequestId.get(htmlRequestId) + + if (htmlRequestId === requestId) { + // The debug channel is for the HTML request. + if (client) { + // If the client is connected, we can connect the debug channel for + // the HTML request immediately. + connectReactDebugChannel( + htmlRequestId, + debugChannel, + sendToClient.bind(null, client) + ) + } else { + // Otherwise, we'll do that when the client connects and just store + // the debug channel. + setReactDebugChannelForHtmlRequest(htmlRequestId, debugChannel) + } + } else if (client) { + // The debug channel is for a subsequent request (e.g. client-side + // navigation for server function call). If the client is not connected + // anymore, we don't need to connect the debug channel. + connectReactDebugChannel( + requestId, + debugChannel, + sendToClient.bind(null, client) + ) } }, @@ -1400,14 +1420,14 @@ export async function createHotReloaderTurbopack( recordMcpTelemetry(opts.telemetry) for (const wsClient of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { // it's okay to not cleanly close these websocket connections, this is dev wsClient.terminate() } - clientsWithoutRequestId.clear() - clientsByRequestId.clear() + clientsWithoutHtmlRequestId.clear() + clientsByHtmlRequestId.clear() }, } @@ -1467,8 +1487,8 @@ export async function createHotReloaderTurbopack( addErrors(errors, currentEntryIssues) for (const client of [ - ...clientsWithoutRequestId, - ...clientsByRequestId.values(), + ...clientsWithoutHtmlRequestId, + ...clientsByHtmlRequestId.values(), ]) { const state = clientStates.get(client) if (!state) { diff --git a/packages/next/src/server/dev/hot-reloader-types.ts b/packages/next/src/server/dev/hot-reloader-types.ts index d24fa646c98274..b36b80761062a3 100644 --- a/packages/next/src/server/dev/hot-reloader-types.ts +++ b/packages/next/src/server/dev/hot-reloader-types.ts @@ -229,11 +229,7 @@ export interface NextJsHotReloaderInterface { * and App Router clients that don't have Cache Components enabled. */ sendToLegacyClients(action: HmrMessageSentToBrowser): void - setCacheStatus( - status: ServerCacheStatus, - htmlRequestId: string, - requestId: string - ): void + setCacheStatus(status: ServerCacheStatus, htmlRequestId: string): void setReactDebugChannel( debugChannel: ReactDebugChannelForBrowser, htmlRequestId: string, diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index 1d8061ad1c0f8f..e0f5cefcc4a06c 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -99,8 +99,9 @@ import { import { InvariantError } from '../../shared/lib/invariant-error' import { connectReactDebugChannel, - deleteReactDebugChannel, - setReactDebugChannel, + connectReactDebugChannelForHtmlRequest, + deleteReactDebugChannelForHtmlRequest, + setReactDebugChannelForHtmlRequest, type ReactDebugChannelForBrowser, } from './debug-channel' import { @@ -444,7 +445,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { ) => void ) { wsServer.handleUpgrade(req, req.socket, head, (client) => { - const requestId = req.url + const htmlRequestId = req.url ? new URL(req.url, 'http://n').searchParams.get('id') : null @@ -452,7 +453,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { throw new InvariantError('Did not start HotReloaderWebpack.') } - this.webpackHotMiddleware.onHMR(client, requestId) + this.webpackHotMiddleware.onHMR(client, htmlRequestId) this.onDemandEntries?.onHMR(client, () => this.hmrServerError) const enableCacheComponents = this.config.cacheComponents @@ -461,7 +462,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { // Router clients are also considered legacy clients. TODO: Maybe mark // clients as App Router / Pages Router clients explicitly, instead of // inferring it from the presence of a request ID. - const isLegacyClient = !requestId || !enableCacheComponents + const isLegacyClient = !htmlRequestId || !enableCacheComponents callback(client, { isLegacyClient }) @@ -639,28 +640,28 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { } }) - if (requestId) { - connectReactDebugChannel( - requestId, + if (htmlRequestId) { + connectReactDebugChannelForHtmlRequest( + htmlRequestId, this.sendToClient.bind(this, client) ) if (enableCacheComponents) { - const status = this.cacheStatusesByRequestId.get(requestId) + const status = this.cacheStatusesByRequestId.get(htmlRequestId) if (status) { this.sendToClient(client, { type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR, state: status, }) - this.cacheStatusesByRequestId.delete(requestId) + this.cacheStatusesByRequestId.delete(htmlRequestId) } } } client.on('close', () => { - this.webpackHotMiddleware?.deleteClient(client, requestId) + this.webpackHotMiddleware?.deleteClient(client, htmlRequestId) - if (requestId) { - deleteReactDebugChannel(requestId) + if (htmlRequestId) { + deleteReactDebugChannelForHtmlRequest(htmlRequestId) } }) }) @@ -1748,8 +1749,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { public setCacheStatus( status: ServerCacheStatus, - htmlRequestId: string, - requestId: string + htmlRequestId: string ): void { const client = this.webpackHotMiddleware?.getClient(htmlRequestId) if (client !== undefined) { @@ -1760,7 +1760,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { } else { // If the client is not connected, store the status so that we can send it // when the client connects. - this.cacheStatusesByRequestId.set(requestId, status) + this.cacheStatusesByRequestId.set(htmlRequestId, status) } } @@ -1769,15 +1769,32 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { htmlRequestId: string, requestId: string ): void { - // Store the debug channel, regardless of whether the client is connected. - setReactDebugChannel(requestId, debugChannel) - - // If the client is connected, we can connect the debug channel immediately. - // Otherwise, we'll do that when the client connects. const client = this.webpackHotMiddleware?.getClient(htmlRequestId) - if (client) { - connectReactDebugChannel(requestId, this.sendToClient.bind(this, client)) + if (htmlRequestId === requestId) { + // The debug channel is for the HTML request. + if (client) { + // If the client is connected, we can connect the debug channel for + // the HTML request immediately. + connectReactDebugChannel( + htmlRequestId, + debugChannel, + this.sendToClient.bind(null, client) + ) + } else { + // Otherwise, we'll do that when the client connects and just store + // the debug channel. + setReactDebugChannelForHtmlRequest(htmlRequestId, debugChannel) + } + } else if (client) { + // The debug channel is for a subsequent request (e.g. client-side + // navigation for server function call). If the client is not connected + // anymore, we don't need to connect the debug channel. + connectReactDebugChannel( + requestId, + debugChannel, + this.sendToClient.bind(null, client) + ) } } diff --git a/packages/next/src/server/lib/dev-bundler-service.ts b/packages/next/src/server/lib/dev-bundler-service.ts index 0b8f53c2a1e18d..24a0ceccad3cc9 100644 --- a/packages/next/src/server/lib/dev-bundler-service.ts +++ b/packages/next/src/server/lib/dev-bundler-service.ts @@ -98,10 +98,9 @@ export class DevBundlerService { public setCacheStatus( status: ServerCacheStatus, - htmlRequestId: string, - requestId: string + htmlRequestId: string ): void { - this.bundler.hotReloader.setCacheStatus(status, htmlRequestId, requestId) + this.bundler.hotReloader.setCacheStatus(status, htmlRequestId) } public setIsrStatus(key: string, value: boolean | undefined) { diff --git a/packages/next/src/server/lib/router-utils/router-server-context.ts b/packages/next/src/server/lib/router-utils/router-server-context.ts index 6d321aac8ad533..5718d78e25e8a1 100644 --- a/packages/next/src/server/lib/router-utils/router-server-context.ts +++ b/packages/next/src/server/lib/router-utils/router-server-context.ts @@ -44,11 +44,7 @@ export type RouterServerContext = Record< htmlRequestId: string, requestId: string ) => void - setCacheStatus?: ( - status: ServerCacheStatus, - htmlRequestId: string, - requestId: string - ) => void + setCacheStatus?: (status: ServerCacheStatus, htmlRequestId: string) => void } > From 4fa0221c366c100d712c88ff5866c4ad4d7017d5 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Wed, 5 Nov 2025 13:36:22 +0100 Subject: [PATCH 2/2] Bind `this`, not `null` --- packages/next/src/server/dev/hot-reloader-webpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index e0f5cefcc4a06c..47b36eebd997ae 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -1779,7 +1779,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { connectReactDebugChannel( htmlRequestId, debugChannel, - this.sendToClient.bind(null, client) + this.sendToClient.bind(this, client) ) } else { // Otherwise, we'll do that when the client connects and just store @@ -1793,7 +1793,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { connectReactDebugChannel( requestId, debugChannel, - this.sendToClient.bind(null, client) + this.sendToClient.bind(this, client) ) } }