Skip to content

Commit 129c46f

Browse files
committed
[Flight] do not emit error after abort
When synchronously aborting in a non-async Function Component if you throw after aborting the task would error rather than abort because React never observed the AbortSignal. This change adds an additional check to see if the request is currently aborting. I left the sigil check in place too in case a task pings after the request is closed (though I'm pretty sure that can't actually happen) I also renamed AbortSigil to AbortSymbol and changed it to a Symbol to make some of the duck typing more efficient.
1 parent 8e60bac commit 129c46f

File tree

2 files changed

+110
-8
lines changed

2 files changed

+110
-8
lines changed

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,4 +2554,100 @@ describe('ReactFlightDOM', () => {
25542554
</div>,
25552555
);
25562556
});
2557+
2558+
it('can error synchronously after aborting in a synchronous Component', async () => {
2559+
const rejectError = new Error('bam!');
2560+
const rejectedPromise = Promise.reject(rejectError);
2561+
rejectedPromise.catch(() => {});
2562+
rejectedPromise.status = 'rejected';
2563+
rejectedPromise.reason = rejectError;
2564+
2565+
const resolvedValue = <p>hello world</p>;
2566+
const resolvedPromise = Promise.resolve(resolvedValue);
2567+
resolvedPromise.status = 'fulfilled';
2568+
resolvedPromise.value = resolvedValue;
2569+
2570+
function App() {
2571+
return (
2572+
<div>
2573+
<Suspense fallback={<p>loading...</p>}>
2574+
<ComponentThatAborts />
2575+
</Suspense>
2576+
<Suspense fallback={<p>loading too...</p>}>
2577+
{rejectedPromise}
2578+
</Suspense>
2579+
<Suspense fallback={<p>loading three...</p>}>
2580+
{resolvedPromise}
2581+
</Suspense>
2582+
</div>
2583+
);
2584+
}
2585+
2586+
const abortRef = {current: null};
2587+
2588+
// This test is specifically asserting that this works with Sync Server Component
2589+
function ComponentThatAborts() {
2590+
abortRef.current();
2591+
throw new Error('boom');
2592+
}
2593+
2594+
const {writable: flightWritable, readable: flightReadable} =
2595+
getTestStream();
2596+
2597+
await serverAct(() => {
2598+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2599+
<App />,
2600+
webpackMap,
2601+
{
2602+
onError(e) {
2603+
console.error(e);
2604+
},
2605+
},
2606+
);
2607+
abortRef.current = abort;
2608+
pipe(flightWritable);
2609+
});
2610+
2611+
assertConsoleErrorDev([
2612+
'The render was aborted by the server without a reason.',
2613+
'bam!',
2614+
]);
2615+
2616+
const response =
2617+
ReactServerDOMClient.createFromReadableStream(flightReadable);
2618+
2619+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2620+
2621+
function ClientApp() {
2622+
return use(response);
2623+
}
2624+
2625+
const shellErrors = [];
2626+
await serverAct(async () => {
2627+
ReactDOMFizzServer.renderToPipeableStream(
2628+
React.createElement(ClientApp),
2629+
{
2630+
onShellError(error) {
2631+
shellErrors.push(error.message);
2632+
},
2633+
},
2634+
).pipe(fizzWritable);
2635+
});
2636+
assertConsoleErrorDev([
2637+
'The render was aborted by the server without a reason.',
2638+
'bam!',
2639+
]);
2640+
2641+
expect(shellErrors).toEqual([]);
2642+
2643+
const container = document.createElement('div');
2644+
await readInto(container, fizzReadable);
2645+
expect(getMeaningfulChildren(container)).toEqual(
2646+
<div>
2647+
<p>loading...</p>
2648+
<p>loading too...</p>
2649+
<p>hello world</p>
2650+
</div>,
2651+
);
2652+
});
25572653
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,6 @@ export type Request = {
382382
didWarnForKey: null | WeakSet<ReactComponentInfo>,
383383
};
384384

385-
const AbortSigil = {};
386-
387385
const {
388386
TaintRegistryObjects,
389387
TaintRegistryValues,
@@ -587,13 +585,16 @@ function serializeThenable(
587585
x !== null &&
588586
(x: any).$$typeof === REACT_POSTPONE_TYPE
589587
) {
588+
newTask.status = COMPLETED;
590589
const postponeInstance: Postpone = (x: any);
591590
logPostpone(request, postponeInstance.message, newTask);
592591
emitPostponeChunk(request, newTask.id, postponeInstance);
593592
} else {
593+
newTask.status = ERRORED;
594594
const digest = logRecoverableError(request, x, null);
595595
emitErrorChunk(request, newTask.id, digest, x);
596596
}
597+
request.abortableTasks.delete(newTask);
597598
return newTask.id;
598599
}
599600
default: {
@@ -1114,7 +1115,8 @@ function renderFunctionComponent<Props>(
11141115
// If we aborted during rendering we should interrupt the render but
11151116
// we don't need to provide an error because the renderer will encode
11161117
// the abort error as the reason.
1117-
throw AbortSigil;
1118+
// eslint-disable-next-line no-throw-literal
1119+
throw null;
11181120
}
11191121

11201122
if (
@@ -1616,7 +1618,8 @@ function renderElement(
16161618
// lazy initializers are user code and could abort during render
16171619
// we don't wan to return any value resolved from the lazy initializer
16181620
// if it aborts so we interrupt rendering here
1619-
throw AbortSigil;
1621+
// eslint-disable-next-line no-throw-literal
1622+
throw null;
16201623
}
16211624
return renderElement(
16221625
request,
@@ -2183,7 +2186,7 @@ function renderModel(
21832186
}
21842187
}
21852188

2186-
if (thrownValue === AbortSigil) {
2189+
if (request.status === ABORTING) {
21872190
task.status = ABORTED;
21882191
const errorId: number = (request.fatalError: any);
21892192
if (wasReactNode) {
@@ -2357,7 +2360,8 @@ function renderModelDestructive(
23572360
// lazy initializers are user code and could abort during render
23582361
// we don't wan to return any value resolved from the lazy initializer
23592362
// if it aborts so we interrupt rendering here
2360-
throw AbortSigil;
2363+
// eslint-disable-next-line no-throw-literal
2364+
throw null;
23612365
}
23622366
if (__DEV__) {
23632367
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
@@ -3690,7 +3694,7 @@ function retryTask(request: Request, task: Task): void {
36903694
}
36913695
}
36923696

3693-
if (x === AbortSigil) {
3697+
if (request.status === ABORTING) {
36943698
request.abortableTasks.delete(task);
36953699
task.status = ABORTED;
36963700
const errorId: number = (request.fatalError: any);
@@ -3909,7 +3913,9 @@ export function stopFlowing(request: Request): void {
39093913
// This is called to early terminate a request. It creates an error at all pending tasks.
39103914
export function abort(request: Request, reason: mixed): void {
39113915
try {
3912-
request.status = ABORTING;
3916+
if (request.status === OPEN) {
3917+
request.status = ABORTING;
3918+
}
39133919
const abortableTasks = request.abortableTasks;
39143920
// We have tasks to abort. We'll emit one error row and then emit a reference
39153921
// to that row from every row that's still remaining.

0 commit comments

Comments
 (0)