@samitouri / QOS-React-2 / commits / a9bbe34622

[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');