@samitouri / QOS-React / commits / 7954db9398

[Fizz] handle throwing after abort during render (#30730)

It is possible to throw after aborting during a render and we were not properly tracking this. We use an AbortSigil to mark whether a rendering task needs to abort but the throw interrupts that and we end up handling an error on the error pathway instead. This change reworks the abort-while-rendering support to be robust to throws after calling abort

Josh Story committed Aug 16, 2024 at 18:29 UTC 7954db9398b9afa962167577a6c6940be3856c39
2 files changed +54 -10
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+42
@@ -8377,6 +8377,48 @@ describe('ReactDOMFizzServer', () => {
8377 );
8378 });
8379
8380 + it('can support throwing after aborting during a render', async () => {
8381 + function App() {
8382 + return (
8383 + <div>
8384 + <Suspense fallback={<p>loading...</p>}>
8385 + <ComponentThatAborts />
8386 + </Suspense>
8387 + </div>
8388 + );
8389 + }
8390 +
8391 + function ComponentThatAborts() {
8392 + abortRef.current('boom');
8393 + throw new Error('bam');
8394 + }
8395 +
8396 + const abortRef = {current: null};
8397 + let finished = false;
8398 + const errors = [];
8399 + await act(() => {
8400 + const {pipe, abort} = renderToPipeableStream(<App />, {
8401 + onError(err) {
8402 + errors.push(err);
8403 + },
8404 + });
8405 + abortRef.current = abort;
8406 + writable.on('finish', () => {
8407 + finished = true;
8408 + });
8409 + pipe(writable);
8410 + });
8411 +
8412 + expect(errors).toEqual(['boom']);
8413 +
8414 + expect(finished).toBe(true);
8415 + expect(getVisibleChildren(container)).toEqual(
8416 + <div>
8417 + <p>loading...</p>
8418 + </div>,
8419 + );
8420 + });
8421 +
8422 it('should warn for using generators as children props', async () => {
8423 function* getChildren() {
8424 yield <h1 key="1">Hello</h1>;
packages/react-server/src/ReactFizzServer.js
+12 -10
@@ -652,8 +652,6 @@ export function resumeRequest(
652 return request;
653 }
654
655 -const AbortSigil = {};
656 -
655 let currentRequest: null | Request = null;
656
657 export function resolveRequest(): null | Request {
@@ -1173,7 +1171,7 @@ function renderSuspenseBoundary(
1171 );
1172 boundarySegment.status = COMPLETED;
1173 } catch (thrownValue: mixed) {
1176 - if (thrownValue === AbortSigil) {
1174 + if (request.status === ABORTING) {
1175 boundarySegment.status = ABORTED;
1176 } else {
1177 boundarySegment.status = ERRORED;
@@ -1246,7 +1244,7 @@ function renderSuspenseBoundary(
1244 } catch (thrownValue: mixed) {
1245 newBoundary.status = CLIENT_RENDERED;
1246 let error: mixed;
1249 - if (thrownValue === AbortSigil) {
1247 + if (request.status === ABORTING) {
1248 contentRootSegment.status = ABORTED;
1249 error = request.fatalError;
1250 } else {
@@ -1601,7 +1599,8 @@ function finishClassComponent(
1599 nextChildren = instance.render();
1600 }
1601 if (request.status === ABORTING) {
1604 - throw AbortSigil;
1602 + // eslint-disable-next-line no-throw-literal
1603 + throw null;
1604 }
1605
1606 if (__DEV__) {
@@ -1757,7 +1756,8 @@ function renderFunctionComponent(
1756 legacyContext,
1757 );
1758 if (request.status === ABORTING) {
1760 - throw AbortSigil;
1759 + // eslint-disable-next-line no-throw-literal
1760 + throw null;
1761 }
1762
1763 const hasId = checkDidRenderIdHook();
@@ -2076,7 +2076,8 @@ function renderLazyComponent(
2076 Component = init(payload);
2077 }
2078 if (request.status === ABORTING) {
2079 - throw AbortSigil;
2079 + // eslint-disable-next-line no-throw-literal
2080 + throw null;
2081 }
2082 const resolvedProps = resolveDefaultPropsOnNonClassComponent(
2083 Component,
@@ -2655,7 +2656,8 @@ function retryNode(request: Request, task: Task): void {
2656 resolvedNode = init(payload);
2657 }
2658 if (request.status === ABORTING) {
2658 - throw AbortSigil;
2659 + // eslint-disable-next-line no-throw-literal
2660 + throw null;
2661 }
2662 // Now we render the resolved node
2663 renderNodeDestructive(request, task, resolvedNode, childIndex);
@@ -4127,7 +4129,7 @@ function retryRenderTask(
4129 // (unstable) API for suspending. This implementation detail can change
4130 // later, once we deprecate the old API in favor of `use`.
4131 getSuspendedThenable()
4130 - : thrownValue === AbortSigil
4132 + : request.status === ABORTING
4133 ? request.fatalError
4134 : thrownValue;
4135
@@ -4250,7 +4252,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
4252 erroredReplay(
4253 request,
4254 task.blockedBoundary,
4253 - x === AbortSigil ? request.fatalError : x,
4255 + request.status === ABORTING ? request.fatalError : x,
4256 errorInfo,
4257 task.replay.nodes,
4258 task.replay.slots,