[Flight] Reject any new Chunks not yet discovered at the time of reportGlobalError (#31851)
Same as #31840 but for the Flight Client.
Sebastian Markbåge committed
Dec 19, 2024 at 00:03 UTC
a9bbe34622885ef5667d33236d580fe7321c0d8b
6 files changed
+70
-57
packages/react-client/src/ReactFlightClient.js
+14
-2
@@ -307,6 +307,8 @@ export type Response = {
307
_rowTag: number, // 0 indicates that we're currently parsing the row ID
308
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
309
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
310
+ _closed: boolean,
311
+ _closedReason: mixed,
312
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
313
_timeOrigin: number, // Profiling-only
314
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
@@ -358,7 +360,7 @@ function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
360
361
function createErrorChunk<T>(
362
response: Response,
361
- error: Error | Postpone,
363
+ error: mixed,
364
): ErroredChunk<T> {
365
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
366
return new ReactPromise(ERRORED, null, error, response);
@@ -639,6 +641,8 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
641
// Report that any missing chunks in the model is now going to throw this
642
// error upon read. Also notify any pending promises.
643
export function reportGlobalError(response: Response, error: Error): void {
644
+ response._closed = true;
645
+ response._closedReason = error;
646
response._chunks.forEach(chunk => {
647
// If this chunk was already resolved or errored, it won't
648
// trigger an error but if it wasn't then we need to
@@ -913,7 +917,13 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
917
const chunks = response._chunks;
918
let chunk = chunks.get(id);
919
if (!chunk) {
916
- chunk = createPendingChunk(response);
920
+ if (response._closed) {
921
+ // We have already errored the response and we're not going to get
922
+ // anything more streaming in so this will immediately error.
923
+ chunk = createErrorChunk(response, response._closedReason);
924
+ } else {
925
+ chunk = createPendingChunk(response);
926
+ }
927
chunks.set(id, chunk);
928
}
929
return chunk;
@@ -1640,6 +1650,8 @@ function ResponseInstance(
1650
this._rowTag = 0;
1651
this._rowLength = 0;
1652
this._buffer = [];
1653
+ this._closed = false;
1654
+ this._closedReason = null;
1655
this._tempRefs = temporaryReferences;
1656
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1657
this._timeOrigin = 0;
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+1
-2
@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
24
25
const decoderOptions = {stream: true};
26
27
-const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
27
+const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
28
createStringDecoder() {
29
return new TextDecoder();
30
},
@@ -74,7 +74,6 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
74
for (let i = 0; i < source.length; i++) {
75
processBinaryChunk(response, source[i], 0);
76
}
77
- close(response);
77
return getRoot(response);
78
}
79
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+3
-21
@@ -2902,16 +2902,7 @@ describe('ReactFlightDOM', () => {
2902
abortFizz('bam');
2903
});
2904
2905
- if (__DEV__) {
2906
- expect(errors).toEqual([new Error('Connection closed.')]);
2907
- } else {
2908
- // This is likely a bug. In Dev we get a connection closed error
2909
- // because the debug info creates a chunk that has a pending status
2910
- // and when the stream finishes we error if any chunks are still pending.
2911
- // In production there is no debug info so the missing chunk is never instantiated
2912
- // because nothing triggers model evaluation before the stream completes
2913
- expect(errors).toEqual(['bam']);
2914
- }
2905
+ expect(errors).toEqual([new Error('Connection closed.')]);
2906
2907
const container = document.createElement('div');
2908
await readInto(container, fizzReadable);
@@ -3066,17 +3057,8 @@ describe('ReactFlightDOM', () => {
3057
});
3058
3059
// one error per boundary
3069
- if (__DEV__) {
3070
- const err = new Error('Connection closed.');
3071
- expect(errors).toEqual([err, err, err]);
3072
- } else {
3073
- // This is likely a bug. In Dev we get a connection closed error
3074
- // because the debug info creates a chunk that has a pending status
3075
- // and when the stream finishes we error if any chunks are still pending.
3076
- // In production there is no debug info so the missing chunk is never instantiated
3077
- // because nothing triggers model evaluation before the stream completes
3078
- expect(errors).toEqual(['boom', 'boom', 'boom']);
3079
- }
3060
+ const err = new Error('Connection closed.');
3061
+ expect(errors).toEqual([err, err, err]);
3062
3063
const container = document.createElement('div');
3064
await readInto(container, fizzReadable);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+2
-12
@@ -2574,17 +2574,7 @@ describe('ReactFlightDOMBrowser', () => {
2574
root.render(<ClientRoot response={response} />);
2575
});
2576
2577
- if (__DEV__) {
2578
- expect(errors).toEqual([new Error('Connection closed.')]);
2579
- expect(container.innerHTML).toBe('');
2580
- } else {
2581
- // This is likely a bug. In Dev we get a connection closed error
2582
- // because the debug info creates a chunk that has a pending status
2583
- // and when the stream finishes we error if any chunks are still pending.
2584
- // In production there is no debug info so the missing chunk is never instantiated
2585
- // because nothing triggers model evaluation before the stream completes
2586
- expect(errors).toEqual([]);
2587
- expect(container.innerHTML).toBe('<div>loading...</div>');
2588
- }
2577
+ expect(errors).toEqual([new Error('Connection closed.')]);
2578
+ expect(container.innerHTML).toBe('');
2579
});
2580
});
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+49
-10
@@ -1245,20 +1245,59 @@ describe('ReactFlightDOMEdge', () => {
1245
),
1246
);
1247
fizzController.abort('bam');
1248
- if (__DEV__) {
1249
- expect(errors).toEqual([new Error('Connection closed.')]);
1250
- } else {
1251
- // This is likely a bug. In Dev we get a connection closed error
1252
- // because the debug info creates a chunk that has a pending status
1253
- // and when the stream finishes we error if any chunks are still pending.
1254
- // In production there is no debug info so the missing chunk is never instantiated
1255
- // because nothing triggers model evaluation before the stream completes
1256
- expect(errors).toEqual(['bam']);
1257
- }
1248
+ expect(errors).toEqual([new Error('Connection closed.')]);
1249
// Should still match the result when parsed
1250
const result = await readResult(ssrStream);
1251
const div = document.createElement('div');
1252
div.innerHTML = result;
1253
expect(div.textContent).toBe('loading...');
1254
});
1255
+
1256
+ // @gate enableHalt
1257
+ it('should abort parsing an incomplete prerender payload', async () => {
1258
+ const infinitePromise = new Promise(() => {});
1259
+ const controller = new AbortController();
1260
+ const errors = [];
1261
+ const {pendingResult} = await serverAct(async () => {
1262
+ // destructure trick to avoid the act scope from awaiting the returned value
1263
+ return {
1264
+ pendingResult: ReactServerDOMStaticServer.unstable_prerender(
1265
+ {promise: infinitePromise},
1266
+ webpackMap,
1267
+ {
1268
+ signal: controller.signal,
1269
+ onError(err) {
1270
+ errors.push(err);
1271
+ },
1272
+ },
1273
+ ),
1274
+ };
1275
+ });
1276
+
1277
+ controller.abort();
1278
+ const {prelude} = await pendingResult;
1279
+
1280
+ expect(errors).toEqual([]);
1281
+
1282
+ const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1283
+ serverConsumerManifest: {
1284
+ moduleMap: {},
1285
+ moduleLoading: {},
1286
+ },
1287
+ });
1288
+
1289
+ // Wait for the stream to finish and therefore abort before we try to .then the response.
1290
+ await 0;
1291
+
1292
+ const result = await response;
1293
+
1294
+ let error = null;
1295
+ try {
1296
+ await result.promise;
1297
+ } catch (x) {
1298
+ error = x;
1299
+ }
1300
+ expect(error).not.toBe(null);
1301
+ expect(error.message).toBe('Connection closed.');
1302
+ });
1303
});
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+1
-10
@@ -508,16 +508,7 @@ describe('ReactFlightDOMNode', () => {
508
),
509
);
510
ssrStream.abort('bam');
511
- if (__DEV__) {
512
- expect(errors).toEqual([new Error('Connection closed.')]);
513
- } else {
514
- // This is likely a bug. In Dev we get a connection closed error
515
- // because the debug info creates a chunk that has a pending status
516
- // and when the stream finishes we error if any chunks are still pending.
517
- // In production there is no debug info so the missing chunk is never instantiated
518
- // because nothing triggers model evaluation before the stream completes
519
- expect(errors).toEqual(['bam']);
520
- }
511
+ expect(errors).toEqual([new Error('Connection closed.')]);
512
// Should still match the result when parsed
513
const result = await readResult(ssrStream);
514
const div = document.createElement('div');