Skip to content

Commit a9bbe34

Browse files
authored
[Flight] Reject any new Chunks not yet discovered at the time of reportGlobalError (facebook#31851)
Same as facebook#31840 but for the Flight Client.
1 parent 17520b6 commit a9bbe34

File tree

6 files changed

+70
-57
lines changed

6 files changed

+70
-57
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ export type Response = {
307307
_rowTag: number, // 0 indicates that we're currently parsing the row ID
308308
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
309309
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
310+
_closed: boolean,
311+
_closedReason: mixed,
310312
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
311313
_timeOrigin: number, // Profiling-only
312314
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
@@ -358,7 +360,7 @@ function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
358360

359361
function createErrorChunk<T>(
360362
response: Response,
361-
error: Error | Postpone,
363+
error: mixed,
362364
): ErroredChunk<T> {
363365
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
364366
return new ReactPromise(ERRORED, null, error, response);
@@ -639,6 +641,8 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
639641
// Report that any missing chunks in the model is now going to throw this
640642
// error upon read. Also notify any pending promises.
641643
export function reportGlobalError(response: Response, error: Error): void {
644+
response._closed = true;
645+
response._closedReason = error;
642646
response._chunks.forEach(chunk => {
643647
// If this chunk was already resolved or errored, it won't
644648
// trigger an error but if it wasn't then we need to
@@ -913,7 +917,13 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
913917
const chunks = response._chunks;
914918
let chunk = chunks.get(id);
915919
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+
}
917927
chunks.set(id, chunk);
918928
}
919929
return chunk;
@@ -1640,6 +1650,8 @@ function ResponseInstance(
16401650
this._rowTag = 0;
16411651
this._rowLength = 0;
16421652
this._buffer = [];
1653+
this._closed = false;
1654+
this._closedReason = null;
16431655
this._tempRefs = temporaryReferences;
16441656
if (enableProfilerTimer && enableComponentPerformanceTrack) {
16451657
this._timeOrigin = 0;

packages/react-noop-renderer/src/ReactNoopFlightClient.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
2424

2525
const decoderOptions = {stream: true};
2626

27-
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
27+
const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
2828
createStringDecoder() {
2929
return new TextDecoder();
3030
},
@@ -74,7 +74,6 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
7474
for (let i = 0; i < source.length; i++) {
7575
processBinaryChunk(response, source[i], 0);
7676
}
77-
close(response);
7877
return getRoot(response);
7978
}
8079

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

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,16 +2902,7 @@ describe('ReactFlightDOM', () => {
29022902
abortFizz('bam');
29032903
});
29042904

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.')]);
29152906

29162907
const container = document.createElement('div');
29172908
await readInto(container, fizzReadable);
@@ -3066,17 +3057,8 @@ describe('ReactFlightDOM', () => {
30663057
});
30673058

30683059
// 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]);
30803062

30813063
const container = document.createElement('div');
30823064
await readInto(container, fizzReadable);

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2574,17 +2574,7 @@ describe('ReactFlightDOMBrowser', () => {
25742574
root.render(<ClientRoot response={response} />);
25752575
});
25762576

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('');
25892579
});
25902580
});

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

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,20 +1245,59 @@ describe('ReactFlightDOMEdge', () => {
12451245
),
12461246
);
12471247
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.')]);
12581249
// Should still match the result when parsed
12591250
const result = await readResult(ssrStream);
12601251
const div = document.createElement('div');
12611252
div.innerHTML = result;
12621253
expect(div.textContent).toBe('loading...');
12631254
});
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+
});
12641303
});

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -508,16 +508,7 @@ describe('ReactFlightDOMNode', () => {
508508
),
509509
);
510510
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.')]);
521512
// Should still match the result when parsed
522513
const result = await readResult(ssrStream);
523514
const div = document.createElement('div');

0 commit comments

Comments
 (0)