Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2554,4 +2554,100 @@ describe('ReactFlightDOM', () => {
</div>,
);
});

it('can error synchronously after aborting in a synchronous Component', async () => {
const rejectError = new Error('bam!');
const rejectedPromise = Promise.reject(rejectError);
rejectedPromise.catch(() => {});
rejectedPromise.status = 'rejected';
rejectedPromise.reason = rejectError;

const resolvedValue = <p>hello world</p>;
const resolvedPromise = Promise.resolve(resolvedValue);
resolvedPromise.status = 'fulfilled';
resolvedPromise.value = resolvedValue;

function App() {
return (
<div>
<Suspense fallback={<p>loading...</p>}>
<ComponentThatAborts />
</Suspense>
<Suspense fallback={<p>loading too...</p>}>
{rejectedPromise}
</Suspense>
<Suspense fallback={<p>loading three...</p>}>
{resolvedPromise}
</Suspense>
</div>
);
}

const abortRef = {current: null};

// This test is specifically asserting that this works with Sync Server Component
function ComponentThatAborts() {
abortRef.current();
throw new Error('boom');
}

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

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

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

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.',
'bam!',
]);

expect(shellErrors).toEqual([]);

const container = document.createElement('div');
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
<div>
<p>loading...</p>
<p>loading too...</p>
<p>hello world</p>
</div>,
);
});
});
23 changes: 14 additions & 9 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,6 @@ export type Request = {
didWarnForKey: null | WeakSet<ReactComponentInfo>,
};

const AbortSigil = {};

const {
TaintRegistryObjects,
TaintRegistryValues,
Expand Down Expand Up @@ -594,6 +592,8 @@ function serializeThenable(
const digest = logRecoverableError(request, x, null);
emitErrorChunk(request, newTask.id, digest, x);
}
newTask.status = ERRORED;
request.abortableTasks.delete(newTask);
return newTask.id;
}
default: {
Expand Down Expand Up @@ -650,10 +650,10 @@ function serializeThenable(
logPostpone(request, postponeInstance.message, newTask);
emitPostponeChunk(request, newTask.id, postponeInstance);
} else {
newTask.status = ERRORED;
const digest = logRecoverableError(request, reason, newTask);
emitErrorChunk(request, newTask.id, digest, reason);
}
newTask.status = ERRORED;
request.abortableTasks.delete(newTask);
enqueueFlush(request);
},
Expand Down Expand Up @@ -1114,7 +1114,8 @@ function renderFunctionComponent<Props>(
// 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.
throw AbortSigil;
// eslint-disable-next-line no-throw-literal
throw null;
}

if (
Expand Down Expand Up @@ -1616,7 +1617,8 @@ function renderElement(
// lazy initializers are user code and could abort during render
// we don't wan to return any value resolved from the lazy initializer
// if it aborts so we interrupt rendering here
throw AbortSigil;
// eslint-disable-next-line no-throw-literal
throw null;
}
return renderElement(
request,
Expand Down Expand Up @@ -2183,7 +2185,7 @@ function renderModel(
}
}

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

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