From 1de109c72afd5d19757e09289cc719b40793c817 Mon Sep 17 00:00:00 2001 From: nehaprasad-dev Date: Wed, 20 May 2026 22:21:46 +0530 Subject: [PATCH] Cap React debug channel data sent over HMR in dev --- .../next/src/server/dev/debug-channel.test.ts | 39 +++++++++ packages/next/src/server/dev/debug-channel.ts | 79 +++++++++++++++++-- 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 packages/next/src/server/dev/debug-channel.test.ts diff --git a/packages/next/src/server/dev/debug-channel.test.ts b/packages/next/src/server/dev/debug-channel.test.ts new file mode 100644 index 000000000000..e7bd5b279be5 --- /dev/null +++ b/packages/next/src/server/dev/debug-channel.test.ts @@ -0,0 +1,39 @@ +import { takeReactDebugChunkForHmr } from './debug-channel' + +describe('takeReactDebugChunkForHmr', () => { + it('passes through chunks under the limit', () => { + const chunk = new Uint8Array([1, 2, 3]) + const result = takeReactDebugChunkForHmr(chunk, 0, 10) + + expect(result.chunk).toBe(chunk) + expect(result.bytesSent).toBe(3) + expect(result.done).toBe(false) + }) + + it('truncates a chunk that would exceed the limit', () => { + const chunk = new Uint8Array([1, 2, 3, 4, 5]) + const result = takeReactDebugChunkForHmr(chunk, 3, 10) + + expect(result.chunk).toEqual(new Uint8Array([4, 5])) + expect(result.bytesSent).toBe(10) + expect(result.done).toBe(true) + }) + + it('marks the stream done once the limit is reached', () => { + const chunk = new Uint8Array([1, 2, 3]) + const result = takeReactDebugChunkForHmr(chunk, 8, 10) + + expect(result.chunk).toEqual(new Uint8Array([1, 2])) + expect(result.bytesSent).toBe(10) + expect(result.done).toBe(true) + }) + + it('returns done when already at the limit', () => { + const chunk = new Uint8Array([1, 2, 3]) + const result = takeReactDebugChunkForHmr(chunk, 10, 10) + + expect(result.chunk).toBeNull() + expect(result.bytesSent).toBe(10) + expect(result.done).toBe(true) + }) +}) diff --git a/packages/next/src/server/dev/debug-channel.ts b/packages/next/src/server/dev/debug-channel.ts index 61510afb2970..bc4f24f31ae9 100644 --- a/packages/next/src/server/dev/debug-channel.ts +++ b/packages/next/src/server/dev/debug-channel.ts @@ -24,6 +24,42 @@ const reactDebugChannelsByHtmlRequestId = new Map< ReactDebugChannelForBrowser >() +// Debug payloads over HMR are only used for React DevTools suspend metadata. +// Large server-only values (e.g. fs.readFile of big JSON) must not be forwarded +// in full or they can crash the browser renderer. +const REACT_DEBUG_CHANNEL_HMR_MAX_BYTES = 1024 * 1024 + +export function takeReactDebugChunkForHmr( + chunk: Uint8Array, + bytesSent: number, + maxBytes: number = REACT_DEBUG_CHANNEL_HMR_MAX_BYTES +): { + chunk: Uint8Array | null + bytesSent: number + done: boolean +} { + if (bytesSent >= maxBytes) { + return { chunk: null, bytesSent, done: true } + } + + const remaining = maxBytes - bytesSent + + if (chunk.byteLength <= remaining) { + const nextBytesSent = bytesSent + chunk.byteLength + return { + chunk, + bytesSent: nextBytesSent, + done: nextBytesSent >= maxBytes, + } + } + + return { + chunk: chunk.subarray(0, remaining), + bytesSent: maxBytes, + done: true, + } +} + export function connectReactDebugChannel( requestId: string, debugChannel: ReactDebugChannelForBrowser, @@ -36,6 +72,9 @@ export function connectReactDebugChannel( ) .getReader() + let bytesSent = 0 + let finished = false + const stop = () => { sendToClient({ type: HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK, @@ -44,23 +83,53 @@ export function connectReactDebugChannel( }) } + const finish = (cancelReader: boolean) => { + if (finished) { + return + } + finished = true + if (cancelReader) { + reader.cancel().catch(() => {}) + } + stop() + } + const onError = (err: unknown) => { console.error(new Error('React debug channel stream error', { cause: err })) - stop() + finish(false) } const progress = (entry: ReadableStreamReadResult) => { + if (finished) { + return + } + if (entry.done) { - stop() - } else { + finish(false) + return + } + + const { + chunk, + bytesSent: nextBytesSent, + done, + } = takeReactDebugChunkForHmr(entry.value, bytesSent) + bytesSent = nextBytesSent + + if (chunk && chunk.byteLength > 0) { sendToClient({ type: HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK, requestId, - chunk: entry.value, + chunk, }) + } - reader.read().then(progress, onError) + if (done) { + finish(true) + return } + + reader.read().then(progress, onError) } reader.read().then(progress, onError)