Include regular stack trace in serialized errors from Fizz (#28684)
We previously only included the component stack. Cleaned up the fields in Fizz server that wasn't using consistent hidden classes in dev vs prod. Added a prefix to errors serialized from server rendering. It can be a bit confusing to see where this error came from otherwise since it didn't come from elsewhere on the client. It's really kind of confusing with other recoverable errors that happen on the client too.
Sebastian Markbåge committed
Mar 30, 2024 at 11:08 UTC
b9149cc6e6442389accf1f7c34a77ba2e6e52b5e
13 files changed
+229
-104
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+9
-2
@@ -1226,15 +1226,21 @@ export function isSuspenseInstanceFallback(
1226
1227
export function getSuspenseInstanceFallbackErrorDetails(
1228
instance: SuspenseInstance,
1229
-): {digest: ?string, message?: string, stack?: string} {
1229
+): {
1230
+ digest: ?string,
1231
+ message?: string,
1232
+ stack?: string,
1233
+ componentStack?: string,
1234
+} {
1235
const dataset =
1236
instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset;
1232
- let digest, message, stack;
1237
+ let digest, message, stack, componentStack;
1238
if (dataset) {
1239
digest = dataset.dgst;
1240
if (__DEV__) {
1241
message = dataset.msg;
1242
stack = dataset.stck;
1243
+ componentStack = dataset.cstck;
1244
}
1245
}
1246
if (__DEV__) {
@@ -1242,6 +1248,7 @@ export function getSuspenseInstanceFallbackErrorDetails(
1248
message,
1249
digest,
1250
stack,
1251
+ componentStack,
1252
};
1253
} else {
1254
// Object gets DCE'd if constructed in tail position and matches callsite destructuring
packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+1
@@ -94,6 +94,7 @@ function handleNode(node_: Node) {
94
dataset['dgst'],
95
dataset['msg'],
96
dataset['stck'],
97
+ dataset['cstck'],
98
);
99
node.remove();
100
} else if (dataset['rri'] != null) {
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+40
-10
@@ -3801,6 +3801,8 @@ const clientRenderedSuspenseBoundaryError1B =
3801
stringToPrecomputedChunk(' data-msg="');
3802
const clientRenderedSuspenseBoundaryError1C =
3803
stringToPrecomputedChunk(' data-stck="');
3804
+const clientRenderedSuspenseBoundaryError1D =
3805
+ stringToPrecomputedChunk(' data-cstck="');
3806
const clientRenderedSuspenseBoundaryError2 =
3807
stringToPrecomputedChunk('></template>');
3808
@@ -3843,7 +3845,8 @@ export function writeStartClientRenderedSuspenseBoundary(
3845
destination: Destination,
3846
renderState: RenderState,
3847
errorDigest: ?string,
3846
- errorMesssage: ?string,
3848
+ errorMessage: ?string,
3849
+ errorStack: ?string,
3850
errorComponentStack: ?string,
3851
): boolean {
3852
let result;
@@ -3861,19 +3864,27 @@ export function writeStartClientRenderedSuspenseBoundary(
3864
);
3865
}
3866
if (__DEV__) {
3864
- if (errorMesssage) {
3867
+ if (errorMessage) {
3868
writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
3869
writeChunk(
3870
destination,
3868
- stringToChunk(escapeTextForBrowser(errorMesssage)),
3871
+ stringToChunk(escapeTextForBrowser(errorMessage)),
3872
);
3873
writeChunk(
3874
destination,
3875
clientRenderedSuspenseBoundaryErrorAttrInterstitial,
3876
);
3877
}
3875
- if (errorComponentStack) {
3878
+ if (errorStack) {
3879
writeChunk(destination, clientRenderedSuspenseBoundaryError1C);
3880
+ writeChunk(destination, stringToChunk(escapeTextForBrowser(errorStack)));
3881
+ writeChunk(
3882
+ destination,
3883
+ clientRenderedSuspenseBoundaryErrorAttrInterstitial,
3884
+ );
3885
+ }
3886
+ if (errorComponentStack) {
3887
+ writeChunk(destination, clientRenderedSuspenseBoundaryError1D);
3888
writeChunk(
3889
destination,
3890
stringToChunk(escapeTextForBrowser(errorComponentStack)),
@@ -4236,6 +4247,7 @@ const clientRenderData1 = stringToPrecomputedChunk(
4247
const clientRenderData2 = stringToPrecomputedChunk('" data-dgst="');
4248
const clientRenderData3 = stringToPrecomputedChunk('" data-msg="');
4249
const clientRenderData4 = stringToPrecomputedChunk('" data-stck="');
4250
+const clientRenderData5 = stringToPrecomputedChunk('" data-cstck="');
4251
const clientRenderDataEnd = dataElementQuotedEnd;
4252
4253
export function writeClientRenderBoundaryInstruction(
@@ -4244,8 +4256,9 @@ export function writeClientRenderBoundaryInstruction(
4256
renderState: RenderState,
4257
id: number,
4258
errorDigest: ?string,
4247
- errorMessage?: string,
4248
- errorComponentStack?: string,
4259
+ errorMessage: ?string,
4260
+ errorStack: ?string,
4261
+ errorComponentStack: ?string,
4262
): boolean {
4263
const scriptFormat =
4264
!enableFizzExternalRuntime ||
@@ -4276,7 +4289,7 @@ export function writeClientRenderBoundaryInstruction(
4289
writeChunk(destination, clientRenderScript1A);
4290
}
4291
4279
- if (errorDigest || errorMessage || errorComponentStack) {
4292
+ if (errorDigest || errorMessage || errorStack || errorComponentStack) {
4293
if (scriptFormat) {
4294
// ,"JSONString"
4295
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
@@ -4293,7 +4306,7 @@ export function writeClientRenderBoundaryInstruction(
4306
);
4307
}
4308
}
4296
- if (errorMessage || errorComponentStack) {
4309
+ if (errorMessage || errorStack || errorComponentStack) {
4310
if (scriptFormat) {
4311
// ,"JSONString"
4312
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
@@ -4310,6 +4323,23 @@ export function writeClientRenderBoundaryInstruction(
4323
);
4324
}
4325
}
4326
+ if (errorStack || errorComponentStack) {
4327
+ // ,"JSONString"
4328
+ if (scriptFormat) {
4329
+ writeChunk(destination, clientRenderErrorScriptArgInterstitial);
4330
+ writeChunk(
4331
+ destination,
4332
+ stringToChunk(escapeJSStringsForInstructionScripts(errorStack || '')),
4333
+ );
4334
+ } else {
4335
+ // " data-stck="HTMLString
4336
+ writeChunk(destination, clientRenderData4);
4337
+ writeChunk(
4338
+ destination,
4339
+ stringToChunk(escapeTextForBrowser(errorStack || '')),
4340
+ );
4341
+ }
4342
+ }
4343
if (errorComponentStack) {
4344
// ,"JSONString"
4345
if (scriptFormat) {
@@ -4321,8 +4351,8 @@ export function writeClientRenderBoundaryInstruction(
4351
),
4352
);
4353
} else {
4324
- // " data-stck="HTMLString
4325
- writeChunk(destination, clientRenderData4);
4354
+ // " data-cstck="HTMLString
4355
+ writeChunk(destination, clientRenderData5);
4356
writeChunk(
4357
destination,
4358
stringToChunk(escapeTextForBrowser(errorComponentStack)),
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+2
@@ -219,6 +219,7 @@ export function writeStartClientRenderedSuspenseBoundary(
219
// flushing these error arguments are not currently supported in this legacy streaming format.
220
errorDigest: ?string,
221
errorMessage: ?string,
222
+ errorStack: ?string,
223
errorComponentStack: ?string,
224
): boolean {
225
if (renderState.generateStaticMarkup) {
@@ -231,6 +232,7 @@ export function writeStartClientRenderedSuspenseBoundary(
232
renderState,
233
errorDigest,
234
errorMessage,
235
+ errorStack,
236
errorComponentStack,
237
);
238
}
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1
-1
@@ -2,7 +2,7 @@
2
// The build script is at scripts/rollup/generate-inline-fizz-runtime.js.
3
// Run `yarn generate-inline-fizz-runtime` to generate.
4
export const clientRenderBoundary =
5
- '$RX=function(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())};';
5
+ '$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
6
export const completeBoundary =
7
'$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
8
export const completeBoundaryWithStyles =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+3
-1
@@ -19,6 +19,7 @@ export function clientRenderBoundary(
19
suspenseBoundaryID,
20
errorDigest,
21
errorMsg,
22
+ errorStack,
23
errorComponentStack,
24
) {
25
// Find the fallback's first element.
@@ -36,7 +37,8 @@ export function clientRenderBoundary(
37
const dataset = suspenseIdNode.dataset;
38
if (errorDigest) dataset['dgst'] = errorDigest;
39
if (errorMsg) dataset['msg'] = errorMsg;
39
- if (errorComponentStack) dataset['stck'] = errorComponentStack;
40
+ if (errorStack) dataset['stck'] = errorStack;
41
+ if (errorComponentStack) dataset['cstck'] = errorComponentStack;
42
// Tell React to retry it if the parent already hydrated.
43
if (suspenseNode['_reactRetry']) {
44
suspenseNode['_reactRetry']();
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+44
-18
@@ -796,7 +796,8 @@ describe('ReactDOMFizzServer', () => {
796
errors,
797
[
798
[
799
- theError.message,
799
+ 'Switched to client rendering because the server rendering errored:\n\n' +
800
+ theError.message,
801
expectedDigest,
802
componentStack(['Lazy', 'Suspense', 'div', 'App']),
803
],
@@ -919,7 +920,8 @@ describe('ReactDOMFizzServer', () => {
920
errors,
921
[
922
[
922
- theError.message,
923
+ 'Switched to client rendering because the server rendering errored:\n\n' +
924
+ theError.message,
925
expectedDigest,
926
componentStack(['Lazy', 'Suspense', 'div', 'App']),
927
],
@@ -1002,7 +1004,8 @@ describe('ReactDOMFizzServer', () => {
1004
errors,
1005
[
1006
[
1005
- theError.message,
1007
+ 'Switched to client rendering because the server rendering errored:\n\n' +
1008
+ theError.message,
1009
expectedDigest,
1010
componentStack([
1011
'Erroring',
@@ -1088,7 +1091,8 @@ describe('ReactDOMFizzServer', () => {
1091
errors,
1092
[
1093
[
1091
- theError.message,
1094
+ 'Switched to client rendering because the server rendering errored:\n\n' +
1095
+ theError.message,
1096
expectedDigest,
1097
componentStack(['Lazy', 'Suspense', 'div', 'App']),
1098
],
@@ -1414,13 +1418,15 @@ describe('ReactDOMFizzServer', () => {
1418
errors,
1419
[
1420
[
1417
- 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1421
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1422
+ 'The render was aborted by the server without a reason.',
1423
expectedDigest,
1424
// We get the stack of the task when it was aborted which is why we see `h1`
1425
componentStack(['h1', 'Suspense', 'div', 'App']),
1426
],
1427
[
1423
- 'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1428
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1429
+ 'The render was aborted by the server without a reason.',
1430
expectedDigest,
1431
componentStack(['Suspense', 'main', 'div', 'App']),
1432
],
@@ -2155,7 +2161,8 @@ describe('ReactDOMFizzServer', () => {
2161
errors,
2162
[
2163
[
2158
- theError.message,
2164
+ 'Switched to client rendering because the server rendering errored:\n\n' +
2165
+ theError.message,
2166
expectedDigest,
2167
componentStack([
2168
'AsyncText',
@@ -3441,12 +3448,14 @@ describe('ReactDOMFizzServer', () => {
3448
errors,
3449
[
3450
[
3444
- 'The server did not finish this Suspense boundary: foobar',
3451
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3452
+ 'foobar',
3453
'a digest',
3454
componentStack(['Suspense', 'p', 'div', 'App']),
3455
],
3456
[
3449
- 'The server did not finish this Suspense boundary: foobar',
3457
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3458
+ 'foobar',
3459
'a digest',
3460
componentStack(['Suspense', 'span', 'div', 'App']),
3461
],
@@ -3522,12 +3531,14 @@ describe('ReactDOMFizzServer', () => {
3531
errors,
3532
[
3533
[
3525
- 'The server did not finish this Suspense boundary: uh oh',
3534
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3535
+ 'uh oh',
3536
'a digest',
3537
componentStack(['Suspense', 'p', 'div', 'App']),
3538
],
3539
[
3530
- 'The server did not finish this Suspense boundary: uh oh',
3540
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
3541
+ 'uh oh',
3542
'a digest',
3543
componentStack(['Suspense', 'span', 'div', 'App']),
3544
],
@@ -4001,7 +4012,8 @@ describe('ReactDOMFizzServer', () => {
4012
errors,
4013
[
4014
[
4004
- theError.message,
4015
+ 'Switched to client rendering because the server rendering errored:\n\n' +
4016
+ theError.message,
4017
expectedDigest,
4018
componentStack(['Erroring', 'Suspense', 'div', 'App']),
4019
],
@@ -6782,7 +6794,14 @@ describe('ReactDOMFizzServer', () => {
6794
6795
expect(recoverableErrors).toEqual(
6796
__DEV__
6785
- ? ['server error', 'replay error', 'server error']
6797
+ ? [
6798
+ 'Switched to client rendering because the server rendering errored:\n\n' +
6799
+ 'server error',
6800
+ 'Switched to client rendering because the server rendering errored:\n\n' +
6801
+ 'replay error',
6802
+ 'Switched to client rendering because the server rendering errored:\n\n' +
6803
+ 'server error',
6804
+ ]
6805
: [
6806
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
6807
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -6941,8 +6960,10 @@ describe('ReactDOMFizzServer', () => {
6960
expect(recoverableErrors).toEqual(
6961
__DEV__
6962
? [
6944
- 'The server did not finish this Suspense boundary: aborted',
6945
- 'The server did not finish this Suspense boundary: aborted',
6963
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
6964
+ 'aborted',
6965
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
6966
+ 'aborted',
6967
]
6968
: [
6969
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -7113,8 +7134,10 @@ describe('ReactDOMFizzServer', () => {
7134
// It surfaced in two different suspense boundaries.
7135
__DEV__
7136
? [
7116
- 'The server did not finish this Suspense boundary: replay error',
7117
- 'The server did not finish this Suspense boundary: replay error',
7137
+ 'Switched to client rendering because the server rendering errored:\n\n' +
7138
+ 'replay error',
7139
+ 'Switched to client rendering because the server rendering errored:\n\n' +
7140
+ 'replay error',
7141
]
7142
: [
7143
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -7240,7 +7263,10 @@ describe('ReactDOMFizzServer', () => {
7263
7264
expect(recoverableErrors).toEqual(
7265
__DEV__
7243
- ? ['server error']
7266
+ ? [
7267
+ 'Switched to client rendering because the server rendering errored:\n\n' +
7268
+ 'server error',
7269
+ ]
7270
: [
7271
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
7272
],
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+12
-8
@@ -1292,10 +1292,12 @@ describe('ReactDOMServerHydration', () => {
1292
}
1293
1294
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1295
- [
1296
- "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1297
- ]
1298
- `);
1295
+ [
1296
+ "Caught [Switched to client rendering because the server rendering aborted due to:
1297
+
1298
+ The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1299
+ ]
1300
+ `);
1301
});
1302
1303
// @gate __DEV__
@@ -1318,10 +1320,12 @@ describe('ReactDOMServerHydration', () => {
1320
}
1321
1322
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1321
- [
1322
- "Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1323
- ]
1324
- `);
1323
+ [
1324
+ "Caught [Switched to client rendering because the server rendering aborted due to:
1325
+
1326
+ The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1327
+ ]
1328
+ `);
1329
});
1330
});
1331
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+10
-5
@@ -2108,7 +2108,8 @@ describe('ReactDOMServerPartialHydration', () => {
2108
});
2109
if (__DEV__) {
2110
await waitForAll([
2111
- 'The server did not finish this Suspense boundary: The server used' +
2111
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2112
+ 'The server used' +
2113
' "renderToString" which does not support Suspense.',
2114
]);
2115
} else {
@@ -2177,7 +2178,8 @@ describe('ReactDOMServerPartialHydration', () => {
2178
});
2179
if (__DEV__) {
2180
await waitForAll([
2180
- 'The server did not finish this Suspense boundary: The server used' +
2181
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2182
+ 'The server used' +
2183
' "renderToString" which does not support Suspense.',
2184
]);
2185
} else {
@@ -2251,7 +2253,8 @@ describe('ReactDOMServerPartialHydration', () => {
2253
});
2254
if (__DEV__) {
2255
await waitForAll([
2254
- 'The server did not finish this Suspense boundary: The server used' +
2256
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2257
+ 'The server used' +
2258
' "renderToString" which does not support Suspense.',
2259
]);
2260
} else {
@@ -2571,7 +2574,8 @@ describe('ReactDOMServerPartialHydration', () => {
2574
suspend = true;
2575
if (__DEV__) {
2576
await waitForAll([
2574
- 'The server did not finish this Suspense boundary: The server used' +
2577
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2578
+ 'The server used' +
2579
' "renderToString" which does not support Suspense.',
2580
]);
2581
} else {
@@ -2641,7 +2645,8 @@ describe('ReactDOMServerPartialHydration', () => {
2645
});
2646
if (__DEV__) {
2647
await waitForAll([
2644
- 'The server did not finish this Suspense boundary: The server used' +
2648
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2649
+ 'The server used' +
2650
' "renderToString" which does not support Suspense.',
2651
]);
2652
} else {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+4
-2
@@ -674,7 +674,8 @@ describe('ReactDOMServerHydration', () => {
674
expect(errors.length).toBe(1);
675
if (__DEV__) {
676
expect(errors[0]).toBe(
677
- 'The server did not finish this Suspense boundary: The server used "renderToString" ' +
677
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
678
+ 'The server used "renderToString" ' +
679
'which does not support Suspense. If you intended for this Suspense boundary to render ' +
680
'the fallback content on the server consider throwing an Error somewhere within the ' +
681
'Suspense boundary. If you intended to have the server wait for the suspended component ' +
@@ -719,7 +720,8 @@ describe('ReactDOMServerHydration', () => {
720
expect(errors.length).toBe(1);
721
if (__DEV__) {
722
expect(errors[0]).toBe(
722
- 'The server did not finish this Suspense boundary: The server used "renderToString" ' +
723
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
724
+ 'The server used "renderToString" ' +
725
'which does not support Suspense. If you intended for this Suspense boundary to render ' +
726
'the fallback content on the server consider throwing an Error somewhere within the ' +
727
'Suspense boundary. If you intended to have the server wait for the suspended component ' +
packages/react-reconciler/src/ReactFiberBeginWork.js
+12
-6
@@ -2735,9 +2735,9 @@ function updateDehydratedSuspenseComponent(
2735
// get an update and we'll never be able to hydrate the final content. Let's just try the
2736
// client side render instead.
2737
let digest: ?string;
2738
- let message, stack;
2738
+ let message, stack, componentStack;
2739
if (__DEV__) {
2740
- ({digest, message, stack} =
2740
+ ({digest, message, stack, componentStack} =
2741
getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
2742
} else {
2743
({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
@@ -2747,18 +2747,24 @@ function updateDehydratedSuspenseComponent(
2747
// TODO: Figure out a better signal than encoding a magic digest value.
2748
if (!enablePostpone || digest !== 'POSTPONE') {
2749
let error;
2750
- if (message) {
2750
+ if (__DEV__ && message) {
2751
// eslint-disable-next-line react-internal/prod-error-codes
2752
error = new Error(message);
2753
} else {
2754
error = new Error(
2755
'The server could not finish this Suspense boundary, likely ' +
2756
- 'due to an error during server rendering. Switched to ' +
2757
- 'client rendering.',
2756
+ 'due to an error during server rendering. ' +
2757
+ 'Switched to client rendering.',
2758
);
2759
}
2760
+ // Replace the stack with the server stack
2761
+ error.stack = (__DEV__ && stack) || '';
2762
(error: any).digest = digest;
2761
- capturedValue = createCapturedValueFromError(error, digest, stack);
2763
+ capturedValue = createCapturedValueFromError(
2764
+ error,
2765
+ digest,
2766
+ componentStack,
2767
+ );
2768
}
2769
return retrySuspenseComponentWithoutHydrating(
2770
current,
packages/react-server/src/ReactFizzServer.js
+90
-50
@@ -203,9 +203,6 @@ const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
203
type SuspenseBoundary = {
204
status: 0 | 1 | 4 | 5,
205
rootSegmentID: number,
206
- errorDigest: ?string, // the error hash if it errors
207
- errorMessage?: string, // the error string if it errors
208
- errorComponentStack?: string, // the error component stack if it errors
206
parentFlushed: boolean,
207
pendingTasks: number, // when it reaches zero we can show this boundary's content
208
completedSegments: Array<Segment>, // completed but not yet flushed segments.
@@ -215,6 +212,11 @@ type SuspenseBoundary = {
212
fallbackState: HoistableState,
213
trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes
214
trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes
215
+ errorDigest: ?string, // the error hash if it errors
216
+ // DEV-only fields
217
+ errorMessage?: null | string, // the error string if it errors
218
+ errorStack?: null | string, // the error stack if it errors
219
+ errorComponentStack?: null | string, // the error component stack if it errors
220
};
221
222
type RenderTask = {
@@ -601,7 +603,7 @@ function createSuspenseBoundary(
603
request: Request,
604
fallbackAbortableTasks: Set<Task>,
605
): SuspenseBoundary {
604
- return {
606
+ const boundary: SuspenseBoundary = {
607
status: PENDING,
608
rootSegmentID: -1,
609
parentFlushed: false,
@@ -615,6 +617,13 @@ function createSuspenseBoundary(
617
trackedContentKeyPath: null,
618
trackedFallbackNode: null,
619
};
620
+ if (__DEV__) {
621
+ // DEV-only fields for hidden class
622
+ boundary.errorMessage = null;
623
+ boundary.errorStack = null;
624
+ boundary.errorComponentStack = null;
625
+ }
626
+ return boundary;
627
}
628
629
function createRenderTask(
@@ -811,22 +820,30 @@ function encodeErrorForBoundary(
820
digest: ?string,
821
error: mixed,
822
thrownInfo: ThrownInfo,
823
+ wasAborted: boolean,
824
) {
825
boundary.errorDigest = digest;
826
if (__DEV__) {
817
- let message;
827
+ let message, stack;
828
// In dev we additionally encode the error message and component stack on the boundary
829
if (error instanceof Error) {
830
// eslint-disable-next-line react-internal/safe-string-coercion
831
message = String(error.message);
832
+ // eslint-disable-next-line react-internal/safe-string-coercion
833
+ stack = String(error.stack);
834
} else if (typeof error === 'object' && error !== null) {
835
message = describeObjectForErrorMessage(error);
836
+ stack = null;
837
} else {
838
// eslint-disable-next-line react-internal/safe-string-coercion
839
message = String(error);
840
+ stack = null;
841
}
828
-
829
- boundary.errorMessage = message;
842
+ const prefix = wasAborted
843
+ ? 'Switched to client rendering because the server rendering aborted due to:\n\n'
844
+ : 'Switched to client rendering because the server rendering errored:\n\n';
845
+ boundary.errorMessage = prefix + message;
846
+ boundary.errorStack = stack;
847
boundary.errorComponentStack = thrownInfo.componentStack;
848
}
849
}
@@ -1007,7 +1024,7 @@ function renderSuspenseBoundary(
1024
} else {
1025
errorDigest = logRecoverableError(request, error, thrownInfo);
1026
}
1010
- encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo);
1027
+ encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false);
1028
1029
untrackBoundary(request, newBoundary);
1030
@@ -1151,7 +1168,13 @@ function replaySuspenseBoundary(
1168
} else {
1169
errorDigest = logRecoverableError(request, error, thrownInfo);
1170
}
1154
- encodeErrorForBoundary(resumedBoundary, errorDigest, error, thrownInfo);
1171
+ encodeErrorForBoundary(
1172
+ resumedBoundary,
1173
+ errorDigest,
1174
+ error,
1175
+ thrownInfo,
1176
+ false,
1177
+ );
1178
1179
task.replay.pendingTasks--;
1180
@@ -2978,6 +3001,7 @@ function erroredReplay(
3001
error,
3002
errorDigest,
3003
errorInfo,
3004
+ false,
3005
);
3006
}
3007
@@ -3008,7 +3032,7 @@ function erroredTask(
3032
boundary.pendingTasks--;
3033
if (boundary.status !== CLIENT_RENDERED) {
3034
boundary.status = CLIENT_RENDERED;
3011
- encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3035
+ encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, false);
3036
untrackBoundary(request, boundary);
3037
3038
// Regardless of what happens next, this boundary won't be displayed,
@@ -3048,6 +3072,7 @@ function abortRemainingSuspenseBoundary(
3072
error: mixed,
3073
errorDigest: ?string,
3074
errorInfo: ThrownInfo,
3075
+ wasAborted: boolean,
3076
): void {
3077
const resumedBoundary = createSuspenseBoundary(request, new Set());
3078
resumedBoundary.parentFlushed = true;
@@ -3055,17 +3080,13 @@ function abortRemainingSuspenseBoundary(
3080
resumedBoundary.rootSegmentID = rootSegmentID;
3081
3082
resumedBoundary.status = CLIENT_RENDERED;
3058
- let errorMessage = error;
3059
- if (__DEV__) {
3060
- const errorPrefix = 'The server did not finish this Suspense boundary: ';
3061
- if (error && typeof error.message === 'string') {
3062
- errorMessage = errorPrefix + error.message;
3063
- } else {
3064
- // eslint-disable-next-line react-internal/safe-string-coercion
3065
- errorMessage = errorPrefix + String(error);
3066
- }
3067
- }
3068
- encodeErrorForBoundary(resumedBoundary, errorDigest, errorMessage, errorInfo);
3083
+ encodeErrorForBoundary(
3084
+ resumedBoundary,
3085
+ errorDigest,
3086
+ error,
3087
+ errorInfo,
3088
+ wasAborted,
3089
+ );
3090
3091
if (resumedBoundary.parentFlushed) {
3092
request.clientRenderedBoundaries.push(resumedBoundary);
@@ -3080,6 +3101,7 @@ function abortRemainingReplayNodes(
3101
error: mixed,
3102
errorDigest: ?string,
3103
errorInfo: ThrownInfo,
3104
+ aborted: boolean,
3105
): void {
3106
for (let i = 0; i < nodes.length; i++) {
3107
const node = nodes[i];
@@ -3092,6 +3114,7 @@ function abortRemainingReplayNodes(
3114
error,
3115
errorDigest,
3116
errorInfo,
3117
+ aborted,
3118
);
3119
} else {
3120
const boundaryNode: ReplaySuspenseBoundary = node;
@@ -3102,6 +3125,7 @@ function abortRemainingReplayNodes(
3125
error,
3126
errorDigest,
3127
errorInfo,
3128
+ aborted,
3129
);
3130
}
3131
}
@@ -3118,7 +3142,7 @@ function abortRemainingReplayNodes(
3142
);
3143
} else if (boundary.status !== CLIENT_RENDERED) {
3144
boundary.status = CLIENT_RENDERED;
3121
- encodeErrorForBoundary(boundary, errorDigest, error, errorInfo);
3145
+ encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, aborted);
3146
if (boundary.parentFlushed) {
3147
request.clientRenderedBoundaries.push(boundary);
3148
}
@@ -3194,6 +3218,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3218
error,
3219
errorDigest,
3220
errorInfo,
3221
+ true,
3222
);
3223
}
3224
request.pendingRootTasks--;
@@ -3223,18 +3248,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3248
} else {
3249
errorDigest = logRecoverableError(request, error, errorInfo);
3250
}
3226
- let errorMessage = error;
3227
- if (__DEV__) {
3228
- const errorPrefix =
3229
- 'The server did not finish this Suspense boundary: ';
3230
- if (error && typeof error.message === 'string') {
3231
- errorMessage = errorPrefix + error.message;
3232
- } else {
3233
- // eslint-disable-next-line react-internal/safe-string-coercion
3234
- errorMessage = errorPrefix + String(error);
3235
- }
3236
- }
3237
- encodeErrorForBoundary(boundary, errorDigest, errorMessage, errorInfo);
3251
+ encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);
3252
3253
untrackBoundary(request, boundary);
3254
@@ -3757,13 +3771,25 @@ function flushSegment(
3771
// Emit a client rendered suspense boundary wrapper.
3772
// We never queue the inner boundary so we'll never emit its content or partial segments.
3773
3760
- writeStartClientRenderedSuspenseBoundary(
3761
- destination,
3762
- request.renderState,
3763
- boundary.errorDigest,
3764
- boundary.errorMessage,
3765
- boundary.errorComponentStack,
3766
- );
3774
+ if (__DEV__) {
3775
+ writeStartClientRenderedSuspenseBoundary(
3776
+ destination,
3777
+ request.renderState,
3778
+ boundary.errorDigest,
3779
+ boundary.errorMessage,
3780
+ boundary.errorStack,
3781
+ boundary.errorComponentStack,
3782
+ );
3783
+ } else {
3784
+ writeStartClientRenderedSuspenseBoundary(
3785
+ destination,
3786
+ request.renderState,
3787
+ boundary.errorDigest,
3788
+ null,
3789
+ null,
3790
+ null,
3791
+ );
3792
+ }
3793
// Flush the fallback.
3794
flushSubtree(request, destination, segment, hoistableState);
3795
@@ -3849,15 +3875,29 @@ function flushClientRenderedBoundary(
3875
destination: Destination,
3876
boundary: SuspenseBoundary,
3877
): boolean {
3852
- return writeClientRenderBoundaryInstruction(
3853
- destination,
3854
- request.resumableState,
3855
- request.renderState,
3856
- boundary.rootSegmentID,
3857
- boundary.errorDigest,
3858
- boundary.errorMessage,
3859
- boundary.errorComponentStack,
3860
- );
3878
+ if (__DEV__) {
3879
+ return writeClientRenderBoundaryInstruction(
3880
+ destination,
3881
+ request.resumableState,
3882
+ request.renderState,
3883
+ boundary.rootSegmentID,
3884
+ boundary.errorDigest,
3885
+ boundary.errorMessage,
3886
+ boundary.errorStack,
3887
+ boundary.errorComponentStack,
3888
+ );
3889
+ } else {
3890
+ return writeClientRenderBoundaryInstruction(
3891
+ destination,
3892
+ request.resumableState,
3893
+ request.renderState,
3894
+ boundary.rootSegmentID,
3895
+ boundary.errorDigest,
3896
+ null,
3897
+ null,
3898
+ null,
3899
+ );
3900
+ }
3901
}
3902
3903
function flushSegmentContainer(
scripts/babel/transform-prevent-infinite-loops.js
+1
-1
@@ -13,7 +13,7 @@
13
// This should be reasonable for all loops in the source.
14
// Note that if the numbers are too large, the tests will take too long to fail
15
// for this to be useful (each individual test case might hit an infinite loop).
16
-const MAX_SOURCE_ITERATIONS = 1500;
16
+const MAX_SOURCE_ITERATIONS = 5000;
17
// Code in tests themselves is permitted to run longer.
18
// For example, in the fuzz tester.
19
const MAX_TEST_ITERATIONS = 5000;