[Flight] Fix debug info filtering to include later resolved I/O (#35036)
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.
Hendrik Liebau committed
Nov 3, 2025 at 22:59 UTC
67f7d47a9b8f450c93ee69edd16aed68e910ff1e
2 files changed
+45
-58
packages/react-client/src/ReactFlightClient.js
+3
-4
@@ -510,7 +510,9 @@ function filterDebugInfo(
510
return;
511
}
512
513
- // Remove any debug info entries that arrived after the defined end time.
513
+ // Remove any debug info entries after the defined end time. For async info
514
+ // that means we're including anything that was awaited before the end time,
515
+ // but it doesn't need to be resolved before the end time.
516
const relativeEndTime =
517
response._debugEndTime -
518
// $FlowFixMe[prop-missing]
@@ -521,9 +523,6 @@ function filterDebugInfo(
523
if (typeof info.time === 'number' && info.time > relativeEndTime) {
524
break;
525
}
524
- if (info.awaited != null && info.awaited.end > relativeEndTime) {
525
- break;
526
- }
526
debugInfo.push(info);
527
}
528
value._debugInfo = debugInfo;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+42
-54
@@ -1197,35 +1197,28 @@ describe('ReactFlightDOMNode', () => {
1197
});
1198
1199
it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => {
1200
- // This test is constructing a scenario where a framework might separate
1201
- // I/O into different phases, e.g. runtime I/O and dynamic I/O. The
1202
- // framework might choose to define an end time for the Flight client,
1203
- // indicating that all I/O info (or any debug info for that matter) that
1204
- // arrives after that time should be ignored. When rendering in Fizz is
1205
- // then aborted, the late-arriving debug info that's used to enhance the
1206
- // owner stack only includes I/O info up to that end time.
1207
- let resolveRuntimeData;
1208
- let resolveDynamicData;
1209
-
1210
- async function getRuntimeData() {
1200
+ let resolveDynamicData1;
1201
+ let resolveDynamicData2;
1202
+
1203
+ async function getDynamicData1() {
1204
return new Promise(resolve => {
1212
- resolveRuntimeData = resolve;
1205
+ resolveDynamicData1 = resolve;
1206
});
1207
}
1208
1216
- async function getDynamicData() {
1209
+ async function getDynamicData2() {
1210
return new Promise(resolve => {
1218
- resolveDynamicData = resolve;
1211
+ resolveDynamicData2 = resolve;
1212
});
1213
}
1214
1215
async function Dynamic() {
1223
- const runtimeData = await getRuntimeData();
1224
- const dynamicData = await getDynamicData();
1216
+ const data1 = await getDynamicData1();
1217
+ const data2 = await getDynamicData2();
1218
1219
return (
1220
<p>
1228
- {runtimeData} {dynamicData}
1221
+ {data1} {data2}
1222
</p>
1223
);
1224
}
@@ -1242,45 +1235,40 @@ describe('ReactFlightDOMNode', () => {
1235
);
1236
}
1237
1245
- const stream = await ReactServerDOMServer.renderToPipeableStream(
1246
- ReactServer.createElement(App),
1247
- webpackMap,
1248
- {filterStackFrame},
1249
- );
1250
-
1238
+ let staticEndTime = -1;
1239
const initialChunks = [];
1240
const dynamicChunks = [];
1253
- let isDynamic = false;
1241
1255
- const passThrough = new Stream.PassThrough(streamOptions);
1256
- stream.pipe(passThrough);
1242
+ await new Promise(resolve => {
1243
+ setTimeout(async () => {
1244
+ const stream = ReactServerDOMServer.renderToPipeableStream(
1245
+ ReactServer.createElement(App),
1246
+ webpackMap,
1247
+ {filterStackFrame},
1248
+ );
1249
1258
- passThrough.on('data', chunk => {
1259
- if (isDynamic) {
1260
- dynamicChunks.push(chunk);
1261
- } else {
1262
- initialChunks.push(chunk);
1263
- }
1264
- });
1250
+ const passThrough = new Stream.PassThrough(streamOptions);
1251
+ stream.pipe(passThrough);
1252
1266
- let endTime;
1253
+ passThrough.on('data', chunk => {
1254
+ if (staticEndTime < 0) {
1255
+ initialChunks.push(chunk);
1256
+ } else {
1257
+ dynamicChunks.push(chunk);
1258
+ }
1259
+ });
1260
1268
- await new Promise(resolve => {
1269
- setTimeout(() => {
1270
- resolveRuntimeData('Hi');
1261
+ passThrough.on('end', resolve);
1262
});
1263
setTimeout(() => {
1273
- isDynamic = true;
1274
- endTime = performance.now() + performance.timeOrigin;
1275
- resolveDynamicData('Josh');
1276
- resolve();
1264
+ staticEndTime = performance.now() + performance.timeOrigin;
1265
+ resolveDynamicData1('Hi');
1266
+ setTimeout(() => {
1267
+ resolveDynamicData2('Josh');
1268
+ });
1269
});
1270
});
1271
1280
- await new Promise(resolve => {
1281
- passThrough.on('end', resolve);
1282
- });
1283
-
1272
// Create a new Readable and push all initial chunks immediately.
1273
const readable = new Stream.Readable({...streamOptions, read() {}});
1274
for (let i = 0; i < initialChunks.length; i++) {
@@ -1311,8 +1299,8 @@ describe('ReactFlightDOMNode', () => {
1299
},
1300
{
1301
// Debug info arriving after this end time will be ignored, e.g. the
1314
- // I/O info for the dynamic data.
1315
- endTime,
1302
+ // I/O info for the second dynamic data.
1303
+ endTime: staticEndTime,
1304
},
1305
);
1306
@@ -1358,12 +1346,12 @@ describe('ReactFlightDOMNode', () => {
1346
'\n' +
1347
' in Dynamic' +
1348
(gate(flags => flags.enableAsyncDebugInfo)
1361
- ? ' (file://ReactFlightDOMNode-test.js:1223:33)\n'
1349
+ ? ' (file://ReactFlightDOMNode-test.js:1216:27)\n'
1350
: '\n') +
1351
' in body\n' +
1352
' in html\n' +
1365
- ' in App (file://ReactFlightDOMNode-test.js:1240:25)\n' +
1366
- ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1353
+ ' in App (file://ReactFlightDOMNode-test.js:1233:25)\n' +
1354
+ ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
1355
);
1356
} else {
1357
expect(
@@ -1372,7 +1360,7 @@ describe('ReactFlightDOMNode', () => {
1360
'\n' +
1361
' in body\n' +
1362
' in html\n' +
1375
- ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1363
+ ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
1364
);
1365
}
1366
@@ -1382,8 +1370,8 @@ describe('ReactFlightDOMNode', () => {
1370
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1371
).toBe(
1372
'\n' +
1385
- ' in Dynamic (file://ReactFlightDOMNode-test.js:1223:33)\n' +
1386
- ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1373
+ ' in Dynamic (file://ReactFlightDOMNode-test.js:1216:27)\n' +
1374
+ ' in App (file://ReactFlightDOMNode-test.js:1233:25)',
1375
);
1376
} else {
1377
expect(
@@ -1391,7 +1379,7 @@ describe('ReactFlightDOMNode', () => {
1379
).toBe(
1380
'' +
1381
'\n' +
1394
- ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1382
+ ' in App (file://ReactFlightDOMNode-test.js:1233:25)',
1383
);
1384
}
1385
} else {