Skip to content

Commit

Permalink
[Flight] erroring after abort should not result in unhandled rejection (
Browse files Browse the repository at this point in the history
#30675)

When I implemented the ability to abort synchronoulsy in flight I made
it possible for erroring async server components to cause an unhandled
rejection error. In the current implementation if you abort during the
synchronous phase of a Function Component and then throw an error in the
synchronous phase React will not attach any promise handlers because it
short circuits the thenable treatment and throws an AbortSigil instead.
This change updates the rendering logic to ignore the rejecting
component.
  • Loading branch information
gnoff authored Aug 13, 2024
1 parent a601d1d commit f6d1df6
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2485,4 +2485,73 @@ describe('ReactFlightDOM', () => {
</div>,
);
});

it('can error synchronously after aborting without an unhandled rejection error', async () => {
function App() {
return (
<div>
<Suspense fallback={<p>loading...</p>}>
<ComponentThatAborts />
</Suspense>
</div>
);
}

const abortRef = {current: null};

async function ComponentThatAborts() {
abortRef.current();
throw new Error('boom');
}

const {writable: flightWritable, readable: flightReadable} =
getTestStream();

await serverAct(() => {
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
<App />,
webpackMap,
);
abortRef.current = abort;
pipe(flightWritable);
});

assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);

const response =
ReactServerDOMClient.createFromReadableStream(flightReadable);

const {writable: fizzWritable, readable: fizzReadable} = getTestStream();

function ClientApp() {
return use(response);
}

const shellErrors = [];
await serverAct(async () => {
ReactDOMFizzServer.renderToPipeableStream(
React.createElement(ClientApp),
{
onShellError(error) {
shellErrors.push(error.message);
},
},
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
]);

expect(shellErrors).toEqual([]);

const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading...</p>
</div>,
);
});
});
31 changes: 19 additions & 12 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,8 @@ function callWithDebugContextInDEV<A, T>(
}
}

const voidHandler = () => {};

function renderFunctionComponent<Props>(
request: Request,
task: Task,
Expand Down Expand Up @@ -1101,6 +1103,14 @@ function renderFunctionComponent<Props>(
}

if (request.status === ABORTING) {
if (
typeof result === 'object' &&
result !== null &&
typeof result.then === 'function' &&
!isClientReference(result)
) {
result.then(voidHandler, voidHandler);
}
// If we aborted during rendering we should interrupt the render but
// we don't need to provide an error because the renderer will encode
// the abort error as the reason.
Expand All @@ -1120,18 +1130,15 @@ function renderFunctionComponent<Props>(
// If the thenable resolves to an element, then it was in a static position,
// the return value of a Server Component. That doesn't need further validation
// of keys. The Server Component itself would have had a key.
thenable.then(
resolvedValue => {
if (
typeof resolvedValue === 'object' &&
resolvedValue !== null &&
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
) {
resolvedValue._store.validated = 1;
}
},
() => {},
);
thenable.then(resolvedValue => {
if (
typeof resolvedValue === 'object' &&
resolvedValue !== null &&
resolvedValue.$$typeof === REACT_ELEMENT_TYPE
) {
resolvedValue._store.validated = 1;
}
}, voidHandler);
}
if (thenable.status === 'fulfilled') {
return thenable.value;
Expand Down

0 comments on commit f6d1df6

Please sign in to comment.