Skip to content

Commit 5772a5c

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 5772a5c

File tree

2 files changed

+109
-8
lines changed

2 files changed

+109
-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: 13 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,
@@ -594,6 +592,8 @@ function serializeThenable(
594592
const digest = logRecoverableError(request, x, null);
595593
emitErrorChunk(request, newTask.id, digest, x);
596594
}
595+
newTask.status = ERRORED;
596+
request.abortableTasks.delete(newTask);
597597
return newTask.id;
598598
}
599599
default: {
@@ -1114,7 +1114,8 @@ function renderFunctionComponent<Props>(
11141114
// If we aborted during rendering we should interrupt the render but
11151115
// we don't need to provide an error because the renderer will encode
11161116
// the abort error as the reason.
1117-
throw AbortSigil;
1117+
// eslint-disable-next-line no-throw-literal
1118+
throw null;
11181119
}
11191120

11201121
if (
@@ -1616,7 +1617,8 @@ function renderElement(
16161617
// lazy initializers are user code and could abort during render
16171618
// we don't wan to return any value resolved from the lazy initializer
16181619
// if it aborts so we interrupt rendering here
1619-
throw AbortSigil;
1620+
// eslint-disable-next-line no-throw-literal
1621+
throw null;
16201622
}
16211623
return renderElement(
16221624
request,
@@ -2183,7 +2185,7 @@ function renderModel(
21832185
}
21842186
}
21852187

2186-
if (thrownValue === AbortSigil) {
2188+
if (request.status === ABORTING) {
21872189
task.status = ABORTED;
21882190
const errorId: number = (request.fatalError: any);
21892191
if (wasReactNode) {
@@ -2357,7 +2359,8 @@ function renderModelDestructive(
23572359
// lazy initializers are user code and could abort during render
23582360
// we don't wan to return any value resolved from the lazy initializer
23592361
// if it aborts so we interrupt rendering here
2360-
throw AbortSigil;
2362+
// eslint-disable-next-line no-throw-literal
2363+
throw null;
23612364
}
23622365
if (__DEV__) {
23632366
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
@@ -3690,7 +3693,7 @@ function retryTask(request: Request, task: Task): void {
36903693
}
36913694
}
36923695

3693-
if (x === AbortSigil) {
3696+
if (request.status === ABORTING) {
36943697
request.abortableTasks.delete(task);
36953698
task.status = ABORTED;
36963699
const errorId: number = (request.fatalError: any);
@@ -3909,7 +3912,9 @@ export function stopFlowing(request: Request): void {
39093912
// This is called to early terminate a request. It creates an error at all pending tasks.
39103913
export function abort(request: Request, reason: mixed): void {
39113914
try {
3912-
request.status = ABORTING;
3915+
if (request.status === OPEN) {
3916+
request.status = ABORTING;
3917+
}
39133918
const abortableTasks = request.abortableTasks;
39143919
// We have tasks to abort. We'll emit one error row and then emit a reference
39153920
// to that row from every row that's still remaining.

0 commit comments

Comments
 (0)