Skip to content

Commit 453a19a

Browse files
authored
[Flight] Collect Debug Info from Rejections in Aborted Render (#33708)
This delays the abort by splitting the abort into a first step that just flags a task as abort and tracks the time that we aborted. This first step also invokes the `cacheSignal()` abort handler. Then in a macrotask do we finish flushing the abort (or halt). This ensures that any microtasks after the abort signal can finish flushing which may emit rejections or fulfill (e.g. if you try/catch the abort or if it was allSettled). These rejections are themselves signals for which promise was blocked on what promise which forms a graph that we can use for debug info. Notably this doesn't include any additional data in the output since we don't include any data produced after the abort. It just uses the additional execution to collect more debug info. The abort itself might not have been spawned from I/O but it's still interesting to mark Promises that aborted as interesting since they may have been blocked on I/O. So we take the inner most Promise that resolved after the end time (presumably due to the abort signal but also could've just finished after but that's still after the abort). Since the microtasks can spawn new Promises after the ones that reject we ignore any of those that started after the abort.
1 parent 5d87cd2 commit 453a19a

File tree

6 files changed

+296
-45
lines changed

6 files changed

+296
-45
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2876,7 +2876,9 @@ describe('ReactFlightDOM', () => {
28762876
};
28772877
});
28782878

2879-
controller.abort('boom');
2879+
await serverAct(() => {
2880+
controller.abort('boom');
2881+
});
28802882
resolveGreeting();
28812883
const {prelude} = await pendingResult;
28822884

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2549,7 +2549,7 @@ describe('ReactFlightDOMBrowser', () => {
25492549

25502550
controller.abort('boom');
25512551
resolveGreeting();
2552-
const {prelude} = await pendingResult;
2552+
const {prelude} = await serverAct(() => pendingResult);
25532553
expect(errors).toEqual([]);
25542554

25552555
function ClientRoot({response}) {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,7 +1437,9 @@ describe('ReactFlightDOMEdge', () => {
14371437
};
14381438
});
14391439

1440-
controller.abort('boom');
1440+
await serverAct(() => {
1441+
controller.abort('boom');
1442+
});
14411443
resolveGreeting();
14421444
const {prelude} = await pendingResult;
14431445

@@ -1497,7 +1499,7 @@ describe('ReactFlightDOMEdge', () => {
14971499
});
14981500

14991501
controller.abort();
1500-
const {prelude} = await pendingResult;
1502+
const {prelude} = await serverAct(() => pendingResult);
15011503

15021504
expect(errors).toEqual([]);
15031505

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ describe('ReactFlightDOMNode', () => {
7777
use = React.use;
7878
});
7979

80+
function filterStackFrame(filename, functionName) {
81+
return (
82+
filename !== '' &&
83+
!filename.startsWith('node:') &&
84+
!filename.includes('node_modules') &&
85+
// Filter out our own internal source code since it'll typically be in node_modules
86+
(!filename.includes('/packages/') || filename.includes('/__tests__/')) &&
87+
!filename.includes('/build/')
88+
);
89+
}
90+
8091
function normalizeCodeLocInfo(str) {
8192
return (
8293
str &&
@@ -560,7 +571,7 @@ describe('ReactFlightDOMNode', () => {
560571

561572
controller.abort('boom');
562573
resolveGreeting();
563-
const {prelude} = await pendingResult;
574+
const {prelude} = await serverAct(() => pendingResult);
564575
expect(errors).toEqual([]);
565576

566577
function ClientRoot({response}) {
@@ -711,4 +722,145 @@ describe('ReactFlightDOMNode', () => {
711722
expect(ownerStack).toBeNull();
712723
}
713724
});
725+
726+
// @gate enableHalt && enableAsyncDebugInfo
727+
it('includes deeper location for aborted stacks', async () => {
728+
async function getData() {
729+
const signal = ReactServer.cacheSignal();
730+
await new Promise((resolve, reject) => {
731+
signal.addEventListener('abort', () => reject(signal.reason));
732+
});
733+
}
734+
735+
async function thisShouldNotBeInTheStack() {
736+
await new Promise((resolve, reject) => {
737+
resolve();
738+
});
739+
}
740+
741+
async function Component() {
742+
try {
743+
await getData();
744+
} catch (x) {
745+
await thisShouldNotBeInTheStack(); // This is issued after the rejection so should not be included.
746+
}
747+
return null;
748+
}
749+
750+
function App() {
751+
return ReactServer.createElement(
752+
'html',
753+
null,
754+
ReactServer.createElement(
755+
'body',
756+
null,
757+
ReactServer.createElement(
758+
ReactServer.Suspense,
759+
{fallback: 'Loading...'},
760+
ReactServer.createElement(Component, null),
761+
),
762+
),
763+
);
764+
}
765+
766+
const errors = [];
767+
const serverAbortController = new AbortController();
768+
const {pendingResult} = await serverAct(async () => {
769+
// destructure trick to avoid the act scope from awaiting the returned value
770+
return {
771+
pendingResult: ReactServerDOMStaticServer.unstable_prerender(
772+
ReactServer.createElement(App, null),
773+
webpackMap,
774+
{
775+
signal: serverAbortController.signal,
776+
onError(error) {
777+
errors.push(error);
778+
},
779+
filterStackFrame,
780+
},
781+
),
782+
};
783+
});
784+
785+
await serverAct(
786+
() =>
787+
new Promise(resolve => {
788+
setImmediate(() => {
789+
serverAbortController.abort();
790+
resolve();
791+
});
792+
}),
793+
);
794+
795+
const {prelude} = await pendingResult;
796+
797+
expect(errors).toEqual([]);
798+
799+
function ClientRoot({response}) {
800+
return use(response);
801+
}
802+
803+
const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
804+
await createBufferedUnclosingStream(prelude),
805+
{
806+
serverConsumerManifest: {
807+
moduleMap: null,
808+
moduleLoading: null,
809+
},
810+
},
811+
);
812+
813+
let componentStack;
814+
let ownerStack;
815+
816+
const clientAbortController = new AbortController();
817+
818+
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
819+
React.createElement(ClientRoot, {response: prerenderResponse}),
820+
{
821+
signal: clientAbortController.signal,
822+
onError(error, errorInfo) {
823+
componentStack = errorInfo.componentStack;
824+
ownerStack = React.captureOwnerStack
825+
? React.captureOwnerStack()
826+
: null;
827+
},
828+
},
829+
);
830+
831+
await await serverAct(
832+
async () =>
833+
new Promise(resolve => {
834+
setImmediate(() => {
835+
clientAbortController.abort();
836+
resolve();
837+
});
838+
}),
839+
);
840+
841+
const fizzPrerenderStream = await fizzPrerenderStreamResult;
842+
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
843+
844+
expect(prerenderHTML).toContain('Loading...');
845+
846+
if (__DEV__) {
847+
expect(normalizeCodeLocInfo(componentStack)).toBe(
848+
'\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
849+
);
850+
} else {
851+
expect(normalizeCodeLocInfo(componentStack)).toBe(
852+
'\n in Suspense\n in body\n in html\n in ClientRoot (at **)',
853+
);
854+
}
855+
856+
if (__DEV__) {
857+
expect(normalizeCodeLocInfo(ownerStack)).toBe(
858+
'\n in getData (at **)' +
859+
'\n in Component (at **)' +
860+
'\n in App (at **)',
861+
);
862+
} else {
863+
expect(ownerStack).toBeNull();
864+
}
865+
});
714866
});

packages/react-server/src/ReactFizzServer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,7 @@ function pushHaltedAwaitOnComponentStack(
10191019
stack: bestStack.debugStack,
10201020
};
10211021
task.debugTask = (bestStack.debugTask: any);
1022+
break;
10221023
}
10231024
}
10241025
}

0 commit comments

Comments
 (0)