diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
index bb65ef4b659a7..039ff1fbefe71 100644
--- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
@@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -187,10 +189,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
index ef980764942d7..13aab928e2879 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
@@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
index ef980764942d7..13aab928e2879 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
@@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
index e484d4b7e77d5..290c3f6fcdaf3 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
@@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -189,10 +191,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index faaf8aef01b0d..eb766fc43a86a 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -2722,4 +2722,83 @@ describe('ReactFlightDOM', () => {
await readInto(container, fizzReadable);
expect(getMeaningfulChildren(container)).toEqual(
hello world
);
});
+
+ // @gate experimental
+ it('serializes unfinished tasks with infinite promises when aborting a prerender without a reason', async () => {
+ let resolveGreeting;
+ const greetingPromise = new Promise(resolve => {
+ resolveGreeting = resolve;
+ });
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ async function Greeting() {
+ await greetingPromise;
+ return 'hello world';
+ }
+
+ const controller = new AbortController();
+ const {pendingResult} = await serverAct(async () => {
+ // destructure trick to avoid the act scope from awaiting the returned value
+ return {
+ pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
+ ,
+ webpackMap,
+ {
+ signal: controller.signal,
+ },
+ ),
+ };
+ });
+
+ controller.abort();
+ resolveGreeting();
+ const {prelude} = await pendingResult;
+
+ const preludeWeb = Readable.toWeb(prelude);
+ const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
+
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
+
+ function ClientApp() {
+ return use(response);
+ }
+
+ const shellErrors = [];
+ let abortFizz;
+ await serverAct(async () => {
+ const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
+ React.createElement(ClientApp),
+ {
+ onShellError(error) {
+ shellErrors.push(error.message);
+ },
+ },
+ );
+ pipe(fizzWritable);
+ abortFizz = abort;
+ });
+
+ await serverAct(() => {
+ try {
+ React.unstable_postpone('abort reason');
+ } catch (reason) {
+ abortFizz(reason);
+ }
+ });
+
+ expect(shellErrors).toEqual([]);
+
+ const container = document.createElement('div');
+ await readInto(container, fizzReadable);
+ expect(getMeaningfulChildren(container)).toEqual(loading...
);
+ });
});
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
index a4e0c3bef693b..14b6d7a70bd2b 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
@@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
index a4e0c3bef693b..14b6d7a70bd2b 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
@@ -18,6 +18,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -146,10 +148,20 @@ function prerender(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
index 1506259476703..0e2a763b5ac68 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
@@ -26,6 +26,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ suspend,
+ isDefaultAbortError,
} from 'react-server/src/ReactFlightServer';
import {
@@ -189,10 +191,20 @@ function prerenderToNodeStream(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
} else {
const listener = () => {
- abort(request, (signal: any).reason);
+ const reason = (signal: any).reason;
+ if (isDefaultAbortError(reason)) {
+ suspend(request);
+ } else {
+ abort(request, reason);
+ }
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 5920cd0a2fbaf..723bdbb4402c1 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -748,22 +748,30 @@ function serializeReadableStream(
}
aborted = true;
request.abortListeners.delete(error);
- if (
+
+ let cancelWith: mixed;
+ if (reason === suspendedRenderSymbol) {
+ cancelWith = suspendedRenderReason;
+ } else if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
+ cancelWith = reason;
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance);
+ enqueueFlush(request);
} else {
+ cancelWith = reason;
const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason);
+ enqueueFlush(request);
}
- enqueueFlush(request);
+
// $FlowFixMe should be able to pass mixed
- reader.cancel(reason).then(error, error);
+ reader.cancel(cancelWith).then(error, error);
}
request.abortListeners.add(error);
reader.read().then(progress, error);
@@ -866,24 +874,30 @@ function serializeAsyncIterable(
}
aborted = true;
request.abortListeners.delete(error);
- if (
+ let throwWith: mixed;
+ if (reason === suspendedRenderSymbol) {
+ throwWith = suspendedRenderReason;
+ } else if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
+ throwWith = reason;
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message, streamTask);
emitPostponeChunk(request, streamTask.id, postponeInstance);
+ enqueueFlush(request);
} else {
+ throwWith = reason;
const digest = logRecoverableError(request, reason, streamTask);
emitErrorChunk(request, streamTask.id, digest, reason);
+ enqueueFlush(request);
}
- enqueueFlush(request);
if (typeof (iterator: any).throw === 'function') {
// The iterator protocol doesn't necessarily include this but a generator do.
// $FlowFixMe should be able to pass mixed
- iterator.throw(reason).then(error, error);
+ iterator.throw(throwWith).then(error, error);
}
}
request.abortListeners.add(error);
@@ -2063,12 +2077,18 @@ function serializeBlob(request: Request, blob: Blob): string {
}
aborted = true;
request.abortListeners.delete(error);
- const digest = logRecoverableError(request, reason, newTask);
- emitErrorChunk(request, newTask.id, digest, reason);
- request.abortableTasks.delete(newTask);
- enqueueFlush(request);
+ let cancelWith: mixed;
+ if (reason === suspendedRenderSymbol) {
+ cancelWith = suspendedRenderReason;
+ } else {
+ cancelWith = reason;
+ const digest = logRecoverableError(request, reason, newTask);
+ emitErrorChunk(request, newTask.id, digest, reason);
+ request.abortableTasks.delete(newTask);
+ enqueueFlush(request);
+ }
// $FlowFixMe should be able to pass mixed
- reader.cancel(reason).then(error, error);
+ reader.cancel(cancelWith).then(error, error);
}
request.abortListeners.add(error);
@@ -4007,3 +4027,42 @@ export function abort(request: Request, reason: mixed): void {
fatalError(request, error);
}
}
+
+const suspendedRenderSymbol = Symbol('suspended render');
+const suspendedRenderReason = 'React Server render ended before finishing';
+
+// This is called to early terminate a request. It creates an error at all pending tasks.
+export function suspend(request: Request, reason: 'string'): void {
+ try {
+ 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.
+ if (abortableTasks.size > 0) {
+ request.pendingChunks++;
+ const refId = request.nextChunkId++;
+ request.fatalError = refId;
+ const model = stringify(serializeInfinitePromise());
+ emitModelChunk(request, refId, model);
+ abortableTasks.forEach(task => abortTask(task, request, refId));
+ abortableTasks.clear();
+ }
+ const abortListeners = request.abortListeners;
+ if (abortListeners.size > 0) {
+ abortListeners.forEach(callback => callback(suspendedRenderSymbol));
+ abortListeners.clear();
+ }
+ if (request.destination !== null) {
+ flushCompletedChunks(request, request.destination);
+ }
+ } catch (error) {
+ logRecoverableError(request, error, null);
+ fatalError(request, error);
+ }
+}
+
+export function isDefaultAbortError(error: mixed): boolean {
+ return typeof error === 'object' && error !== null && error.code === 20;
+}