Skip to content

Commit 43e51e0

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 fa6eab5 commit 43e51e0

19 files changed

+848
-108
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
2020

2121
import {Readable} from 'stream';
2222

23+
import {enableHalt} from 'shared/ReactFeatureFlags';
24+
2325
import {
2426
createRequest,
2527
startWork,
2628
startFlowing,
2729
stopFlowing,
2830
abort,
31+
halt,
2932
} from 'react-server/src/ReactFlightServer';
3033

3134
import {
@@ -187,10 +190,20 @@ function prerenderToNodeStream(
187190
if (options && options.signal) {
188191
const signal = options.signal;
189192
if (signal.aborted) {
190-
abort(request, (signal: any).reason);
193+
const reason = (signal: any).reason;
194+
if (enableHalt) {
195+
halt(request, reason);
196+
} else {
197+
abort(request, reason);
198+
}
191199
} else {
192200
const listener = () => {
193-
abort(request, (signal: any).reason);
201+
const reason = (signal: any).reason;
202+
if (enableHalt) {
203+
halt(request, reason);
204+
} else {
205+
abort(request, reason);
206+
}
194207
signal.removeEventListener('abort', listener);
195208
};
196209
signal.addEventListener('abort', listener);

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
1212
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
1313
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
1414

15+
import {enableHalt} from 'shared/ReactFeatureFlags';
16+
1517
import {
1618
createRequest,
1719
startWork,
1820
startFlowing,
1921
stopFlowing,
2022
abort,
23+
halt,
2124
} from 'react-server/src/ReactFlightServer';
2225

2326
import {
@@ -146,10 +149,20 @@ function prerender(
146149
if (options && options.signal) {
147150
const signal = options.signal;
148151
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
152+
const reason = (signal: any).reason;
153+
if (enableHalt) {
154+
halt(request, reason);
155+
} else {
156+
abort(request, reason);
157+
}
150158
} else {
151159
const listener = () => {
152-
abort(request, (signal: any).reason);
160+
const reason = (signal: any).reason;
161+
if (enableHalt) {
162+
halt(request, reason);
163+
} else {
164+
abort(request, reason);
165+
}
153166
signal.removeEventListener('abort', listener);
154167
};
155168
signal.addEventListener('abort', listener);

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes';
1212
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
1313
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
1414

15+
import {enableHalt} from 'shared/ReactFeatureFlags';
16+
1517
import {
1618
createRequest,
1719
startWork,
1820
startFlowing,
1921
stopFlowing,
2022
abort,
23+
halt,
2124
} from 'react-server/src/ReactFlightServer';
2225

2326
import {
@@ -146,10 +149,20 @@ function prerender(
146149
if (options && options.signal) {
147150
const signal = options.signal;
148151
if (signal.aborted) {
149-
abort(request, (signal: any).reason);
152+
const reason = (signal: any).reason;
153+
if (enableHalt) {
154+
halt(request, reason);
155+
} else {
156+
abort(request, reason);
157+
}
150158
} else {
151159
const listener = () => {
152-
abort(request, (signal: any).reason);
160+
const reason = (signal: any).reason;
161+
if (enableHalt) {
162+
halt(request, reason);
163+
} else {
164+
abort(request, reason);
165+
}
153166
signal.removeEventListener('abort', listener);
154167
};
155168
signal.addEventListener('abort', listener);

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes';
2020

2121
import {Readable} from 'stream';
2222

23+
import {enableHalt} from 'shared/ReactFeatureFlags';
24+
2325
import {
2426
createRequest,
2527
startWork,
2628
startFlowing,
2729
stopFlowing,
2830
abort,
31+
halt,
2932
} from 'react-server/src/ReactFlightServer';
3033

3134
import {
@@ -189,10 +192,20 @@ function prerenderToNodeStream(
189192
if (options && options.signal) {
190193
const signal = options.signal;
191194
if (signal.aborted) {
192-
abort(request, (signal: any).reason);
195+
const reason = (signal: any).reason;
196+
if (enableHalt) {
197+
halt(request, reason);
198+
} else {
199+
abort(request, reason);
200+
}
193201
} else {
194202
const listener = () => {
195-
abort(request, (signal: any).reason);
203+
const reason = (signal: any).reason;
204+
if (enableHalt) {
205+
halt(request, reason);
206+
} else {
207+
abort(request, reason);
208+
}
196209
signal.removeEventListener('abort', listener);
197210
};
198211
signal.addEventListener('abort', listener);

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2722,4 +2722,143 @@ describe('ReactFlightDOM', () => {
27222722
await readInto(container, fizzReadable);
27232723
expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
27242724
});
2725+
2726+
// @gate enableHalt
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+
});
2804+
2805+
// @gate enableHalt
2806+
it('will leave async iterables in an incomplete state when halting', async () => {
2807+
let resolve;
2808+
const wait = new Promise(r => (resolve = r));
2809+
const errors = [];
2810+
2811+
const multiShotIterable = {
2812+
async *[Symbol.asyncIterator]() {
2813+
yield {hello: 'A'};
2814+
await wait;
2815+
yield {hi: 'B'};
2816+
return 'C';
2817+
},
2818+
};
2819+
2820+
const controller = new AbortController();
2821+
const {pendingResult} = await serverAct(() => {
2822+
return {
2823+
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2824+
{
2825+
multiShotIterable,
2826+
},
2827+
{},
2828+
{
2829+
onError(x) {
2830+
errors.push(x);
2831+
return x;
2832+
},
2833+
signal: controller.signal,
2834+
},
2835+
),
2836+
};
2837+
});
2838+
2839+
controller.abort();
2840+
await serverAct(() => resolve());
2841+
2842+
const {prelude} = await pendingResult;
2843+
2844+
const result = await ReactServerDOMClient.createFromReadableStream(
2845+
Readable.toWeb(prelude),
2846+
);
2847+
2848+
const iterator = result.multiShotIterable[Symbol.asyncIterator]();
2849+
2850+
expect(await iterator.next()).toEqual({
2851+
value: {hello: 'A'},
2852+
done: false,
2853+
});
2854+
2855+
const race = Promise.race([
2856+
iterator.next(),
2857+
new Promise(r => setTimeout(() => r('timeout'), 0)),
2858+
]);
2859+
2860+
await 1;
2861+
jest.advanceTimersByTime('100');
2862+
expect(await race).toBe('timeout');
2863+
});
27252864
});

0 commit comments

Comments
 (0)