Skip to content

Commit 398aa80

Browse files
committed
[Flight][Static] When prerendering serialize infinite promise when aborting with no reason
When prerendering if you abort the prerender without a reason instead of erroring each remaining task complete it with a promise that never resolve Unfortunately when you abort with an AbortSignal without a value the aborted reason is defaulted to an AbortError DOMexception. We test for this and basically just say that if you abort with an AbortError DOMException that is equivalent to aborting with nothing. Practically this should be fine because usually you abort with a specific reason.
1 parent 0a55ade commit 398aa80

File tree

9 files changed

+248
-25
lines changed

9 files changed

+248
-25
lines changed

packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
startFlowing,
2727
stopFlowing,
2828
abort,
29+
suspend,
30+
isDefaultAbortError,
2931
} from 'react-server/src/ReactFlightServer';
3032

3133
import {
@@ -187,10 +189,20 @@ function prerenderToNodeStream(
187189
if (options && options.signal) {
188190
const signal = options.signal;
189191
if (signal.aborted) {
190-
abort(request, (signal: any).reason);
192+
const reason = (signal: any).reason;
193+
if (isDefaultAbortError(reason)) {
194+
suspend(request);
195+
} else {
196+
abort(request, reason);
197+
}
191198
} else {
192199
const listener = () => {
193-
abort(request, (signal: any).reason);
200+
const reason = (signal: any).reason;
201+
if (isDefaultAbortError(reason)) {
202+
suspend(request);
203+
} else {
204+
abort(request, reason);
205+
}
194206
signal.removeEventListener('abort', listener);
195207
};
196208
signal.addEventListener('abort', listener);

packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
startFlowing,
1919
stopFlowing,
2020
abort,
21+
suspend,
22+
isDefaultAbortError,
2123
} from 'react-server/src/ReactFlightServer';
2224

2325
import {
@@ -146,10 +148,20 @@ function prerender(
146148
if (options && options.signal) {
147149
const signal = options.signal;
148150
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
151+
const reason = (signal: any).reason;
152+
if (isDefaultAbortError(reason)) {
153+
suspend(request);
154+
} else {
155+
abort(request, reason);
156+
}
150157
} else {
151158
const listener = () => {
152-
abort(request, (signal: any).reason);
159+
const reason = (signal: any).reason;
160+
if (isDefaultAbortError(reason)) {
161+
suspend(request);
162+
} else {
163+
abort(request, reason);
164+
}
153165
signal.removeEventListener('abort', listener);
154166
};
155167
signal.addEventListener('abort', listener);

packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
startFlowing,
1919
stopFlowing,
2020
abort,
21+
suspend,
22+
isDefaultAbortError,
2123
} from 'react-server/src/ReactFlightServer';
2224

2325
import {
@@ -146,10 +148,20 @@ function prerender(
146148
if (options && options.signal) {
147149
const signal = options.signal;
148150
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
151+
const reason = (signal: any).reason;
152+
if (isDefaultAbortError(reason)) {
153+
suspend(request);
154+
} else {
155+
abort(request, reason);
156+
}
150157
} else {
151158
const listener = () => {
152-
abort(request, (signal: any).reason);
159+
const reason = (signal: any).reason;
160+
if (isDefaultAbortError(reason)) {
161+
suspend(request);
162+
} else {
163+
abort(request, reason);
164+
}
153165
signal.removeEventListener('abort', listener);
154166
};
155167
signal.addEventListener('abort', listener);

packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
startFlowing,
2727
stopFlowing,
2828
abort,
29+
suspend,
30+
isDefaultAbortError,
2931
} from 'react-server/src/ReactFlightServer';
3032

3133
import {
@@ -189,10 +191,20 @@ function prerenderToNodeStream(
189191
if (options && options.signal) {
190192
const signal = options.signal;
191193
if (signal.aborted) {
192-
abort(request, (signal: any).reason);
194+
const reason = (signal: any).reason;
195+
if (isDefaultAbortError(reason)) {
196+
suspend(request);
197+
} else {
198+
abort(request, reason);
199+
}
193200
} else {
194201
const listener = () => {
195-
abort(request, (signal: any).reason);
202+
const reason = (signal: any).reason;
203+
if (isDefaultAbortError(reason)) {
204+
suspend(request);
205+
} else {
206+
abort(request, reason);
207+
}
196208
signal.removeEventListener('abort', listener);
197209
};
198210
signal.addEventListener('abort', listener);

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2722,4 +2722,83 @@ describe('ReactFlightDOM', () => {
27222722
await readInto(container, fizzReadable);
27232723
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
27242724
});
2725+
2726+
// @gate experimental
2727+
it('serializes unfinished tasks with infinite promises when aborting a prerender without a reason', async () => {
2728+
let resolveGreeting;
2729+
const greetingPromise = new Promise(resolve => {
2730+
resolveGreeting = resolve;
2731+
});
2732+
2733+
function App() {
2734+
return (
2735+
<div>
2736+
<Suspense fallback="loading...">
2737+
<Greeting />
2738+
</Suspense>
2739+
</div>
2740+
);
2741+
}
2742+
2743+
async function Greeting() {
2744+
await greetingPromise;
2745+
return 'hello world';
2746+
}
2747+
2748+
const controller = new AbortController();
2749+
const {pendingResult} = await serverAct(async () => {
2750+
// destructure trick to avoid the act scope from awaiting the returned value
2751+
return {
2752+
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2753+
<App />,
2754+
webpackMap,
2755+
{
2756+
signal: controller.signal,
2757+
},
2758+
),
2759+
};
2760+
});
2761+
2762+
controller.abort();
2763+
resolveGreeting();
2764+
const {prelude} = await pendingResult;
2765+
2766+
const preludeWeb = Readable.toWeb(prelude);
2767+
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
2768+
2769+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2770+
2771+
function ClientApp() {
2772+
return use(response);
2773+
}
2774+
2775+
const shellErrors = [];
2776+
let abortFizz;
2777+
await serverAct(async () => {
2778+
const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
2779+
React.createElement(ClientApp),
2780+
{
2781+
onShellError(error) {
2782+
shellErrors.push(error.message);
2783+
},
2784+
},
2785+
);
2786+
pipe(fizzWritable);
2787+
abortFizz = abort;
2788+
});
2789+
2790+
await serverAct(() => {
2791+
try {
2792+
React.unstable_postpone('abort reason');
2793+
} catch (reason) {
2794+
abortFizz(reason);
2795+
}
2796+
});
2797+
2798+
expect(shellErrors).toEqual([]);
2799+
2800+
const container = document.createElement('div');
2801+
await readInto(container, fizzReadable);
2802+
expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
2803+
});
27252804
});

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
startFlowing,
1919
stopFlowing,
2020
abort,
21+
suspend,
22+
isDefaultAbortError,
2123
} from 'react-server/src/ReactFlightServer';
2224

2325
import {
@@ -146,10 +148,20 @@ function prerender(
146148
if (options && options.signal) {
147149
const signal = options.signal;
148150
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
151+
const reason = (signal: any).reason;
152+
if (isDefaultAbortError(reason)) {
153+
suspend(request);
154+
} else {
155+
abort(request, reason);
156+
}
150157
} else {
151158
const listener = () => {
152-
abort(request, (signal: any).reason);
159+
const reason = (signal: any).reason;
160+
if (isDefaultAbortError(reason)) {
161+
suspend(request);
162+
} else {
163+
abort(request, reason);
164+
}
153165
signal.removeEventListener('abort', listener);
154166
};
155167
signal.addEventListener('abort', listener);

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
startFlowing,
1919
stopFlowing,
2020
abort,
21+
suspend,
22+
isDefaultAbortError,
2123
} from 'react-server/src/ReactFlightServer';
2224

2325
import {
@@ -146,10 +148,20 @@ function prerender(
146148
if (options && options.signal) {
147149
const signal = options.signal;
148150
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
151+
const reason = (signal: any).reason;
152+
if (isDefaultAbortError(reason)) {
153+
suspend(request);
154+
} else {
155+
abort(request, reason);
156+
}
150157
} else {
151158
const listener = () => {
152-
abort(request, (signal: any).reason);
159+
const reason = (signal: any).reason;
160+
if (isDefaultAbortError(reason)) {
161+
suspend(request);
162+
} else {
163+
abort(request, reason);
164+
}
153165
signal.removeEventListener('abort', listener);
154166
};
155167
signal.addEventListener('abort', listener);

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
startFlowing,
2727
stopFlowing,
2828
abort,
29+
suspend,
30+
isDefaultAbortError,
2931
} from 'react-server/src/ReactFlightServer';
3032

3133
import {
@@ -189,10 +191,20 @@ function prerenderToNodeStream(
189191
if (options && options.signal) {
190192
const signal = options.signal;
191193
if (signal.aborted) {
192-
abort(request, (signal: any).reason);
194+
const reason = (signal: any).reason;
195+
if (isDefaultAbortError(reason)) {
196+
suspend(request);
197+
} else {
198+
abort(request, reason);
199+
}
193200
} else {
194201
const listener = () => {
195-
abort(request, (signal: any).reason);
202+
const reason = (signal: any).reason;
203+
if (isDefaultAbortError(reason)) {
204+
suspend(request);
205+
} else {
206+
abort(request, reason);
207+
}
196208
signal.removeEventListener('abort', listener);
197209
};
198210
signal.addEventListener('abort', listener);

0 commit comments

Comments
 (0)