From 8815f2ccf4b9b007686ce8c17b9350b90d1e4228 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 3 Nov 2025 22:23:41 +0100 Subject: [PATCH 1/3] [Flight] Fix debug info filtering to include later resolved I/O In #35019, we excluded debug I/O info from being considered for enhancing the owner stack if it resolved after the defined `endTime` option that can be passed to the Flight client. However, we should include any I/O that was awaited before that end time, even if it resolved later. --- .../react-client/src/ReactFlightClient.js | 7 +- .../src/__tests__/ReactFlightDOMNode-test.js | 96 +++++++++---------- 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 2a5936be0f48..06782b69c693 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -510,7 +510,9 @@ function filterDebugInfo( return; } - // Remove any debug info entries that arrived after the defined end time. + // Remove any debug info entries after the defined end time. For async info + // that means, we're including anything that was awaited before the end time, + // but it must not be resolved before the end time. const relativeEndTime = response._debugEndTime - // $FlowFixMe[prop-missing] @@ -521,9 +523,6 @@ function filterDebugInfo( if (typeof info.time === 'number' && info.time > relativeEndTime) { break; } - if (info.awaited != null && info.awaited.end > relativeEndTime) { - break; - } debugInfo.push(info); } value._debugInfo = debugInfo; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 646735a54b76..c013d2a1eeb5 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -1197,35 +1197,28 @@ describe('ReactFlightDOMNode', () => { }); it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => { - // This test is constructing a scenario where a framework might separate - // I/O into different phases, e.g. runtime I/O and dynamic I/O. The - // framework might choose to define an end time for the Flight client, - // indicating that all I/O info (or any debug info for that matter) that - // arrives after that time should be ignored. When rendering in Fizz is - // then aborted, the late-arriving debug info that's used to enhance the - // owner stack only includes I/O info up to that end time. - let resolveRuntimeData; - let resolveDynamicData; - - async function getRuntimeData() { + let resolveDynamicData1; + let resolveDynamicData2; + + async function getDynamicData1() { return new Promise(resolve => { - resolveRuntimeData = resolve; + resolveDynamicData1 = resolve; }); } - async function getDynamicData() { + async function getDynamicData2() { return new Promise(resolve => { - resolveDynamicData = resolve; + resolveDynamicData2 = resolve; }); } async function Dynamic() { - const runtimeData = await getRuntimeData(); - const dynamicData = await getDynamicData(); + const data1 = await getDynamicData1(); + const data2 = await getDynamicData2(); return (

- {runtimeData} {dynamicData} + {data1} {data2}

); } @@ -1242,45 +1235,42 @@ describe('ReactFlightDOMNode', () => { ); } - const stream = await ReactServerDOMServer.renderToPipeableStream( - ReactServer.createElement(App), - webpackMap, - {filterStackFrame}, - ); + let passThrough; + let staticEndTime = -1; const initialChunks = []; const dynamicChunks = []; - let isDynamic = false; - const passThrough = new Stream.PassThrough(streamOptions); - stream.pipe(passThrough); + await new Promise(resolve => { + setTimeout(async () => { + const stream = ReactServerDOMServer.renderToPipeableStream( + ReactServer.createElement(App), + webpackMap, + {filterStackFrame}, + ); - passThrough.on('data', chunk => { - if (isDynamic) { - dynamicChunks.push(chunk); - } else { - initialChunks.push(chunk); - } - }); + passThrough = new Stream.PassThrough(streamOptions); + stream.pipe(passThrough); - let endTime; + passThrough.on('data', chunk => { + if (staticEndTime < 0) { + initialChunks.push(chunk); + } else { + dynamicChunks.push(chunk); + } + }); - await new Promise(resolve => { - setTimeout(() => { - resolveRuntimeData('Hi'); + passThrough.on('end', resolve); }); setTimeout(() => { - isDynamic = true; - endTime = performance.now() + performance.timeOrigin; - resolveDynamicData('Josh'); - resolve(); + staticEndTime = performance.now() + performance.timeOrigin; + resolveDynamicData1('Hi'); + setTimeout(() => { + resolveDynamicData2('Josh'); + }); }); }); - await new Promise(resolve => { - passThrough.on('end', resolve); - }); - // Create a new Readable and push all initial chunks immediately. const readable = new Stream.Readable({...streamOptions, read() {}}); for (let i = 0; i < initialChunks.length; i++) { @@ -1311,8 +1301,8 @@ describe('ReactFlightDOMNode', () => { }, { // Debug info arriving after this end time will be ignored, e.g. the - // I/O info for the dynamic data. - endTime, + // I/O info for the second dynamic data. + endTime: staticEndTime, }, ); @@ -1358,12 +1348,12 @@ describe('ReactFlightDOMNode', () => { '\n' + ' in Dynamic' + (gate(flags => flags.enableAsyncDebugInfo) - ? ' (file://ReactFlightDOMNode-test.js:1223:33)\n' + ? ' (file://ReactFlightDOMNode-test.js:1216:27)\n' : '\n') + ' in body\n' + ' in html\n' + - ' in App (file://ReactFlightDOMNode-test.js:1240:25)\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)', + ' in App (file://ReactFlightDOMNode-test.js:1233:25)\n' + + ' in ClientRoot (ReactFlightDOMNode-test.js:1310:16)', ); } else { expect( @@ -1372,7 +1362,7 @@ describe('ReactFlightDOMNode', () => { '\n' + ' in body\n' + ' in html\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)', + ' in ClientRoot (ReactFlightDOMNode-test.js:1310:16)', ); } @@ -1382,8 +1372,8 @@ describe('ReactFlightDOMNode', () => { normalizeCodeLocInfo(ownerStack, {preserveLocation: true}), ).toBe( '\n' + - ' in Dynamic (file://ReactFlightDOMNode-test.js:1223:33)\n' + - ' in App (file://ReactFlightDOMNode-test.js:1240:25)', + ' in Dynamic (file://ReactFlightDOMNode-test.js:1216:27)\n' + + ' in App (file://ReactFlightDOMNode-test.js:1233:25)', ); } else { expect( @@ -1391,7 +1381,7 @@ describe('ReactFlightDOMNode', () => { ).toBe( '' + '\n' + - ' in App (file://ReactFlightDOMNode-test.js:1240:25)', + ' in App (file://ReactFlightDOMNode-test.js:1233:25)', ); } } else { From 942afe5e3b26ce32a095c883e41fed8042dcf02d Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 3 Nov 2025 22:41:58 +0100 Subject: [PATCH 2/3] Fix comment --- packages/react-client/src/ReactFlightClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 06782b69c693..f8cc0a80f393 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -511,8 +511,8 @@ function filterDebugInfo( } // Remove any debug info entries after the defined end time. For async info - // that means, we're including anything that was awaited before the end time, - // but it must not be resolved before the end time. + // that means we're including anything that was awaited before the end time, + // but it doesn't need to be resolved before the end time. const relativeEndTime = response._debugEndTime - // $FlowFixMe[prop-missing] From 2e334bd4303fed4e31f05f618e06b949365bb4fd Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 3 Nov 2025 22:49:45 +0100 Subject: [PATCH 3/3] `passThrough` is not needed in the parent scope --- .../src/__tests__/ReactFlightDOMNode-test.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index c013d2a1eeb5..d9de7df43d38 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -1235,9 +1235,7 @@ describe('ReactFlightDOMNode', () => { ); } - let passThrough; let staticEndTime = -1; - const initialChunks = []; const dynamicChunks = []; @@ -1249,7 +1247,7 @@ describe('ReactFlightDOMNode', () => { {filterStackFrame}, ); - passThrough = new Stream.PassThrough(streamOptions); + const passThrough = new Stream.PassThrough(streamOptions); stream.pipe(passThrough); passThrough.on('data', chunk => { @@ -1353,7 +1351,7 @@ describe('ReactFlightDOMNode', () => { ' in body\n' + ' in html\n' + ' in App (file://ReactFlightDOMNode-test.js:1233:25)\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1310:16)', + ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)', ); } else { expect( @@ -1362,7 +1360,7 @@ describe('ReactFlightDOMNode', () => { '\n' + ' in body\n' + ' in html\n' + - ' in ClientRoot (ReactFlightDOMNode-test.js:1310:16)', + ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)', ); }