[Flight] Don't call onError/onPostpone when halting and unify error branches (#31715)
We shouldn't call onError/onPostpone when we halt a stream because that node didn't error yet. Its digest would also get lost. We also have a lot of error branches now for thenables and streams. This unifies them under erroredTask. I'm not yet unifying the cases that don't allocate a task for the error when those are outlined.
Sebastian Markbåge committed
Dec 10, 2024 at 11:59 UTC
4a8fc0f92e0f75257962522b51a938bf4dfda77a
5 files changed
+78
-168
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+2
-2
@@ -2870,7 +2870,7 @@ describe('ReactFlightDOM', () => {
2870
resolveGreeting();
2871
const {prelude} = await pendingResult;
2872
2873
- expect(errors).toEqual(['boom']);
2873
+ expect(errors).toEqual([]);
2874
2875
const preludeWeb = Readable.toWeb(prelude);
2876
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
@@ -3032,7 +3032,7 @@ describe('ReactFlightDOM', () => {
3032
3033
const {prelude} = await pendingResult;
3034
3035
- expect(errors).toEqual(['boom']);
3035
+ expect(errors).toEqual([]);
3036
3037
const preludeWeb = Readable.toWeb(prelude);
3038
const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+1
-1
@@ -2559,7 +2559,7 @@ describe('ReactFlightDOMBrowser', () => {
2559
controller.abort('boom');
2560
resolveGreeting();
2561
const {prelude} = await pendingResult;
2562
- expect(errors).toEqual(['boom']);
2562
+ expect(errors).toEqual([]);
2563
2564
function ClientRoot({response}) {
2565
return use(response);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+1
-1
@@ -1209,7 +1209,7 @@ describe('ReactFlightDOMEdge', () => {
1209
resolveGreeting();
1210
const {prelude} = await pendingResult;
1211
1212
- expect(errors).toEqual(['boom']);
1212
+ expect(errors).toEqual([]);
1213
1214
function ClientRoot({response}) {
1215
return use(response);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+1
-1
@@ -485,7 +485,7 @@ describe('ReactFlightDOMNode', () => {
485
controller.abort('boom');
486
resolveGreeting();
487
const {prelude} = await pendingResult;
488
- expect(errors).toEqual(['boom']);
488
+ expect(errors).toEqual([]);
489
490
function ClientRoot({response}) {
491
return use(response);
packages/react-server/src/ReactFlightServer.js
+73
-163
@@ -629,21 +629,7 @@ function serializeThenable(
629
}
630
case 'rejected': {
631
const x = thenable.reason;
632
- if (
633
- enablePostpone &&
634
- typeof x === 'object' &&
635
- x !== null &&
636
- (x: any).$$typeof === REACT_POSTPONE_TYPE
637
- ) {
638
- const postponeInstance: Postpone = (x: any);
639
- logPostpone(request, postponeInstance.message, newTask);
640
- emitPostponeChunk(request, newTask.id, postponeInstance);
641
- } else {
642
- const digest = logRecoverableError(request, x, null);
643
- emitErrorChunk(request, newTask.id, digest, x);
644
- }
645
- newTask.status = ERRORED;
646
- request.abortableTasks.delete(newTask);
632
+ erroredTask(request, newTask, x);
633
return newTask.id;
634
}
635
default: {
@@ -698,21 +684,7 @@ function serializeThenable(
684
// We expect that the only status it might be otherwise is ABORTED.
685
// When we abort we emit chunks in each pending task slot and don't need
686
// to do so again here.
701
- if (
702
- enablePostpone &&
703
- typeof reason === 'object' &&
704
- reason !== null &&
705
- (reason: any).$$typeof === REACT_POSTPONE_TYPE
706
- ) {
707
- const postponeInstance: Postpone = (reason: any);
708
- logPostpone(request, postponeInstance.message, newTask);
709
- emitPostponeChunk(request, newTask.id, postponeInstance);
710
- } else {
711
- const digest = logRecoverableError(request, reason, newTask);
712
- emitErrorChunk(request, newTask.id, digest, reason);
713
- }
714
- newTask.status = ERRORED;
715
- request.abortableTasks.delete(newTask);
687
+ erroredTask(request, newTask, reason);
688
enqueueFlush(request);
689
}
690
},
@@ -795,8 +767,7 @@ function serializeReadableStream(
767
}
768
aborted = true;
769
request.abortListeners.delete(abortStream);
798
- const digest = logRecoverableError(request, reason, streamTask);
799
- emitErrorChunk(request, streamTask.id, digest, reason);
770
+ erroredTask(request, streamTask, reason);
771
enqueueFlush(request);
772
773
// $FlowFixMe should be able to pass mixed
@@ -808,30 +779,12 @@ function serializeReadableStream(
779
}
780
aborted = true;
781
request.abortListeners.delete(abortStream);
811
- if (
812
- enablePostpone &&
813
- typeof reason === 'object' &&
814
- reason !== null &&
815
- (reason: any).$$typeof === REACT_POSTPONE_TYPE
816
- ) {
817
- const postponeInstance: Postpone = (reason: any);
818
- logPostpone(request, postponeInstance.message, streamTask);
819
- if (enableHalt && request.type === PRERENDER) {
820
- request.pendingChunks--;
821
- } else {
822
- emitPostponeChunk(request, streamTask.id, postponeInstance);
823
- enqueueFlush(request);
824
- }
782
+ if (enableHalt && request.type === PRERENDER) {
783
+ request.pendingChunks--;
784
} else {
826
- const digest = logRecoverableError(request, reason, streamTask);
827
- if (enableHalt && request.type === PRERENDER) {
828
- request.pendingChunks--;
829
- } else {
830
- emitErrorChunk(request, streamTask.id, digest, reason);
831
- enqueueFlush(request);
832
- }
785
+ erroredTask(request, streamTask, reason);
786
+ enqueueFlush(request);
787
}
834
-
788
// $FlowFixMe should be able to pass mixed
789
reader.cancel(reason).then(error, error);
790
}
@@ -937,8 +890,7 @@ function serializeAsyncIterable(
890
}
891
aborted = true;
892
request.abortListeners.delete(abortIterable);
940
- const digest = logRecoverableError(request, reason, streamTask);
941
- emitErrorChunk(request, streamTask.id, digest, reason);
893
+ erroredTask(request, streamTask, reason);
894
enqueueFlush(request);
895
if (typeof (iterator: any).throw === 'function') {
896
// The iterator protocol doesn't necessarily include this but a generator do.
@@ -952,28 +904,11 @@ function serializeAsyncIterable(
904
}
905
aborted = true;
906
request.abortListeners.delete(abortIterable);
955
- if (
956
- enablePostpone &&
957
- typeof reason === 'object' &&
958
- reason !== null &&
959
- (reason: any).$$typeof === REACT_POSTPONE_TYPE
960
- ) {
961
- const postponeInstance: Postpone = (reason: any);
962
- logPostpone(request, postponeInstance.message, streamTask);
963
- if (enableHalt && request.type === PRERENDER) {
964
- request.pendingChunks--;
965
- } else {
966
- emitPostponeChunk(request, streamTask.id, postponeInstance);
967
- enqueueFlush(request);
968
- }
907
+ if (enableHalt && request.type === PRERENDER) {
908
+ request.pendingChunks--;
909
} else {
970
- const digest = logRecoverableError(request, reason, streamTask);
971
- if (enableHalt && request.type === PRERENDER) {
972
- request.pendingChunks--;
973
- } else {
974
- emitErrorChunk(request, streamTask.id, digest, reason);
975
- enqueueFlush(request);
976
- }
910
+ erroredTask(request, streamTask, reason);
911
+ enqueueFlush(request);
912
}
913
if (typeof (iterator: any).throw === 'function') {
914
// The iterator protocol doesn't necessarily include this but a generator do.
@@ -2281,8 +2216,7 @@ function serializeBlob(request: Request, blob: Blob): string {
2216
}
2217
aborted = true;
2218
request.abortListeners.delete(abortBlob);
2284
- const digest = logRecoverableError(request, reason, newTask);
2285
- emitErrorChunk(request, newTask.id, digest, reason);
2219
+ erroredTask(request, newTask, reason);
2220
enqueueFlush(request);
2221
// $FlowFixMe should be able to pass mixed
2222
reader.cancel(reason).then(error, error);
@@ -2293,28 +2227,11 @@ function serializeBlob(request: Request, blob: Blob): string {
2227
}
2228
aborted = true;
2229
request.abortListeners.delete(abortBlob);
2296
- if (
2297
- enablePostpone &&
2298
- typeof reason === 'object' &&
2299
- reason !== null &&
2300
- (reason: any).$$typeof === REACT_POSTPONE_TYPE
2301
- ) {
2302
- const postponeInstance: Postpone = (reason: any);
2303
- logPostpone(request, postponeInstance.message, newTask);
2304
- if (enableHalt && request.type === PRERENDER) {
2305
- request.pendingChunks--;
2306
- } else {
2307
- emitPostponeChunk(request, newTask.id, postponeInstance);
2308
- enqueueFlush(request);
2309
- }
2230
+ if (enableHalt && request.type === PRERENDER) {
2231
+ request.pendingChunks--;
2232
} else {
2311
- const digest = logRecoverableError(request, reason, newTask);
2312
- if (enableHalt && request.type === PRERENDER) {
2313
- request.pendingChunks--;
2314
- } else {
2315
- emitErrorChunk(request, newTask.id, digest, reason);
2316
- enqueueFlush(request);
2317
- }
2233
+ erroredTask(request, newTask, reason);
2234
+ enqueueFlush(request);
2235
}
2236
// $FlowFixMe should be able to pass mixed
2237
reader.cancel(reason).then(error, error);
@@ -2414,24 +2331,6 @@ function renderModel(
2331
return serializeLazyID(newTask.id);
2332
}
2333
return serializeByValueID(newTask.id);
2417
- } else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
2418
- // Something postponed. We'll still send everything we have up until this point.
2419
- // We'll replace this element with a lazy reference that postpones on the client.
2420
- const postponeInstance: Postpone = (x: any);
2421
- request.pendingChunks++;
2422
- const postponeId = request.nextChunkId++;
2423
- logPostpone(request, postponeInstance.message, task);
2424
- emitPostponeChunk(request, postponeId, postponeInstance);
2425
-
2426
- // Restore the context. We assume that this will be restored by the inner
2427
- // functions in case nothing throws so we don't use "finally" here.
2428
- task.keyPath = prevKeyPath;
2429
- task.implicitSlot = prevImplicitSlot;
2430
-
2431
- if (wasReactNode) {
2432
- return serializeLazyID(postponeId);
2433
- }
2434
- return serializeByValueID(postponeId);
2334
}
2335
}
2336
@@ -2443,8 +2342,21 @@ function renderModel(
2342
// Something errored. We'll still send everything we have up until this point.
2343
request.pendingChunks++;
2344
const errorId = request.nextChunkId++;
2446
- const digest = logRecoverableError(request, x, task);
2447
- emitErrorChunk(request, errorId, digest, x);
2345
+ if (
2346
+ enablePostpone &&
2347
+ typeof x === 'object' &&
2348
+ x !== null &&
2349
+ x.$$typeof === REACT_POSTPONE_TYPE
2350
+ ) {
2351
+ // Something postponed. We'll still send everything we have up until this point.
2352
+ // We'll replace this element with a lazy reference that postpones on the client.
2353
+ const postponeInstance: Postpone = (x: any);
2354
+ logPostpone(request, postponeInstance.message, task);
2355
+ emitPostponeChunk(request, errorId, postponeInstance);
2356
+ } else {
2357
+ const digest = logRecoverableError(request, x, task);
2358
+ emitErrorChunk(request, errorId, digest, x);
2359
+ }
2360
if (wasReactNode) {
2361
// We'll replace this element with a lazy reference that throws on the client
2362
// once it gets rendered.
@@ -3964,6 +3876,24 @@ function emitChunk(
3876
emitModelChunk(request, task.id, json);
3877
}
3878
3879
+function erroredTask(request: Request, task: Task, error: mixed): void {
3880
+ request.abortableTasks.delete(task);
3881
+ task.status = ERRORED;
3882
+ if (
3883
+ enablePostpone &&
3884
+ typeof error === 'object' &&
3885
+ error !== null &&
3886
+ error.$$typeof === REACT_POSTPONE_TYPE
3887
+ ) {
3888
+ const postponeInstance: Postpone = (error: any);
3889
+ logPostpone(request, postponeInstance.message, task);
3890
+ emitPostponeChunk(request, task.id, postponeInstance);
3891
+ } else {
3892
+ const digest = logRecoverableError(request, error, task);
3893
+ emitErrorChunk(request, task.id, digest, error);
3894
+ }
3895
+}
3896
+
3897
const emptyRoot = {};
3898
3899
function retryTask(request: Request, task: Task): void {
@@ -4083,20 +4013,9 @@ function retryTask(request: Request, task: Task): void {
4013
const ping = task.ping;
4014
x.then(ping, ping);
4015
return;
4086
- } else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
4087
- request.abortableTasks.delete(task);
4088
- task.status = ERRORED;
4089
- const postponeInstance: Postpone = (x: any);
4090
- logPostpone(request, postponeInstance.message, task);
4091
- emitPostponeChunk(request, task.id, postponeInstance);
4092
- return;
4016
}
4017
}
4095
-
4096
- request.abortableTasks.delete(task);
4097
- task.status = ERRORED;
4098
- const digest = logRecoverableError(request, x, task);
4099
- emitErrorChunk(request, task.id, digest, x);
4018
+ erroredTask(request, task, x);
4019
} finally {
4020
if (__DEV__) {
4021
debugID = prevDebugID;
@@ -4336,7 +4255,12 @@ export function abort(request: Request, reason: mixed): void {
4255
}
4256
const abortableTasks = request.abortableTasks;
4257
if (abortableTasks.size > 0) {
4339
- if (
4258
+ if (enableHalt && request.type === PRERENDER) {
4259
+ // When prerendering with halt semantics we simply halt the task
4260
+ // and leave the reference unfulfilled.
4261
+ abortableTasks.forEach(task => haltTask(task, request));
4262
+ abortableTasks.clear();
4263
+ } else if (
4264
enablePostpone &&
4265
typeof reason === 'object' &&
4266
reason !== null &&
@@ -4344,21 +4268,14 @@ export function abort(request: Request, reason: mixed): void {
4268
) {
4269
const postponeInstance: Postpone = (reason: any);
4270
logPostpone(request, postponeInstance.message, null);
4347
- if (enableHalt && request.type === PRERENDER) {
4348
- // When prerendering with halt semantics we simply halt the task
4349
- // and leave the reference unfulfilled.
4350
- abortableTasks.forEach(task => haltTask(task, request));
4351
- abortableTasks.clear();
4352
- } else {
4353
- // When rendering we produce a shared postpone chunk and then
4354
- // fulfill each task with a reference to that chunk.
4355
- const errorId = request.nextChunkId++;
4356
- request.fatalError = errorId;
4357
- request.pendingChunks++;
4358
- emitPostponeChunk(request, errorId, postponeInstance);
4359
- abortableTasks.forEach(task => abortTask(task, request, errorId));
4360
- abortableTasks.clear();
4361
- }
4271
+ // When rendering we produce a shared postpone chunk and then
4272
+ // fulfill each task with a reference to that chunk.
4273
+ const errorId = request.nextChunkId++;
4274
+ request.fatalError = errorId;
4275
+ request.pendingChunks++;
4276
+ emitPostponeChunk(request, errorId, postponeInstance);
4277
+ abortableTasks.forEach(task => abortTask(task, request, errorId));
4278
+ abortableTasks.clear();
4279
} else {
4280
const error =
4281
reason === undefined
@@ -4373,21 +4290,14 @@ export function abort(request: Request, reason: mixed): void {
4290
)
4291
: reason;
4292
const digest = logRecoverableError(request, error, null);
4376
- if (enableHalt && request.type === PRERENDER) {
4377
- // When prerendering with halt semantics we simply halt the task
4378
- // and leave the reference unfulfilled.
4379
- abortableTasks.forEach(task => haltTask(task, request));
4380
- abortableTasks.clear();
4381
- } else {
4382
- // When rendering we produce a shared error chunk and then
4383
- // fulfill each task with a reference to that chunk.
4384
- const errorId = request.nextChunkId++;
4385
- request.fatalError = errorId;
4386
- request.pendingChunks++;
4387
- emitErrorChunk(request, errorId, digest, error);
4388
- abortableTasks.forEach(task => abortTask(task, request, errorId));
4389
- abortableTasks.clear();
4390
- }
4293
+ // When rendering we produce a shared error chunk and then
4294
+ // fulfill each task with a reference to that chunk.
4295
+ const errorId = request.nextChunkId++;
4296
+ request.fatalError = errorId;
4297
+ request.pendingChunks++;
4298
+ emitErrorChunk(request, errorId, digest, error);
4299
+ abortableTasks.forEach(task => abortTask(task, request, errorId));
4300
+ abortableTasks.clear();
4301
}
4302
const onAllReady = request.onAllReady;
4303
onAllReady();