Skip to content

Commit f8d4a93

Browse files
committed
Implement error digests for Flight runtime
Similar to Fizz, Flight now supports a return value from the user provided onError option. If a value is returned from onError it will be serialized and provided to the client. Currently the digest is stashed on the client constructed Error object but when Error Boundaries are updated to have an errorInfo argument it should probably be moved there.
1 parent b274890 commit f8d4a93

File tree

8 files changed

+265
-46
lines changed

8 files changed

+265
-46
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,21 +628,60 @@ export function resolveSymbol(
628628
chunks.set(id, createInitializedChunk(response, Symbol.for(name)));
629629
}
630630

631-
export function resolveError(
631+
type ErrorWithDigest = Error & {_digest?: string};
632+
export function resolveErrorProd(
632633
response: Response,
633634
id: number,
635+
digest: string,
636+
): void {
637+
if (__DEV__) {
638+
// These errors should never make it into a build so we don't need to encode them in codes.json
639+
// eslint-disable-next-line react-internal/prod-error-codes
640+
throw new Error(
641+
'resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.',
642+
);
643+
}
644+
const error = new Error('An error occurred in the Server Components render.');
645+
error.stack = '';
646+
(error: any)._digest = digest;
647+
const errorWithDigest: ErrorWithDigest = (error: any);
648+
const chunks = response._chunks;
649+
const chunk = chunks.get(id);
650+
if (!chunk) {
651+
chunks.set(id, createErrorChunk(response, errorWithDigest));
652+
} else {
653+
triggerErrorOnChunk(chunk, errorWithDigest);
654+
}
655+
}
656+
657+
export function resolveErrorDev(
658+
response: Response,
659+
id: number,
660+
digest: string,
634661
message: string,
635662
stack: string,
636663
): void {
664+
if (!__DEV__) {
665+
// These errors should never make it into a build so we don't need to encode them in codes.json
666+
// eslint-disable-next-line react-internal/prod-error-codes
667+
throw new Error(
668+
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
669+
);
670+
}
637671
// eslint-disable-next-line react-internal/prod-error-codes
638-
const error = new Error(message);
672+
const error = new Error(
673+
message ||
674+
'An error occurred in the Server Components render but no message was provided',
675+
);
639676
error.stack = stack;
677+
(error: any)._digest = digest;
678+
const errorWithDigest: ErrorWithDigest = (error: any);
640679
const chunks = response._chunks;
641680
const chunk = chunks.get(id);
642681
if (!chunk) {
643-
chunks.set(id, createErrorChunk(response, error));
682+
chunks.set(id, createErrorChunk(response, errorWithDigest));
644683
} else {
645-
triggerErrorOnChunk(chunk, error);
684+
triggerErrorOnChunk(chunk, errorWithDigest);
646685
}
647686
}
648687

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
resolveModel,
1717
resolveProvider,
1818
resolveSymbol,
19-
resolveError,
19+
resolveErrorProd,
20+
resolveErrorDev,
2021
createResponse as createResponseBase,
2122
parseModelString,
2223
parseModelTuple,
@@ -62,7 +63,17 @@ function processFullRow(response: Response, row: string): void {
6263
}
6364
case 'E': {
6465
const errorInfo = JSON.parse(text);
65-
resolveError(response, id, errorInfo.message, errorInfo.stack);
66+
if (__DEV__) {
67+
resolveErrorDev(
68+
response,
69+
id,
70+
errorInfo.digest,
71+
errorInfo.message,
72+
errorInfo.stack,
73+
);
74+
} else {
75+
resolveErrorProd(response, id, errorInfo.digest);
76+
}
6677
return;
6778
}
6879
default: {

packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,48 @@ export function resolveModuleMetaData<T>(
6060

6161
export type Chunk = RowEncoding;
6262

63-
export function processErrorChunk(
63+
export function processErrorChunkProd(
6464
request: Request,
6565
id: number,
66+
digest: string,
67+
): Chunk {
68+
if (__DEV__) {
69+
// These errors should never make it into a build so we don't need to encode them in codes.json
70+
// eslint-disable-next-line react-internal/prod-error-codes
71+
throw new Error(
72+
'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
73+
);
74+
}
75+
76+
return [
77+
'E',
78+
id,
79+
{
80+
digest,
81+
},
82+
];
83+
}
84+
85+
export function processErrorChunkDev(
86+
request: Request,
87+
id: number,
88+
digest: string,
6689
message: string,
6790
stack: string,
6891
): Chunk {
92+
if (!__DEV__) {
93+
// These errors should never make it into a build so we don't need to encode them in codes.json
94+
// eslint-disable-next-line react-internal/prod-error-codes
95+
throw new Error(
96+
'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
97+
);
98+
}
99+
69100
return [
70101
'E',
71102
id,
72103
{
104+
digest,
73105
message,
74106
stack,
75107
},

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,8 @@ describe('ReactFlightDOM', () => {
772772
webpackMap,
773773
{
774774
onError(x) {
775-
reportedErrors.push(x);
775+
reportedErrors.push(x.message);
776+
return `digestOf("${x.message}")`;
776777
},
777778
},
778779
);
@@ -789,15 +790,29 @@ describe('ReactFlightDOM', () => {
789790

790791
await act(async () => {
791792
root.render(
792-
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
793+
<ErrorBoundary
794+
fallback={e => (
795+
<>
796+
<p>{e.message}</p>
797+
<p>{e._digest}</p>
798+
</>
799+
)}>
793800
<Suspense fallback={<p>(loading)</p>}>
794801
<App res={response} />
795802
</Suspense>
796803
</ErrorBoundary>,
797804
);
798805
});
799-
expect(container.innerHTML).toBe('<p>bug in the bundler</p>');
806+
if (__DEV__) {
807+
expect(container.innerHTML).toBe(
808+
'<p>bug in the bundler</p><p>digestOf("bug in the bundler")</p>',
809+
);
810+
} else {
811+
expect(container.innerHTML).toBe(
812+
'<p>An error occurred in the Server Components render.</p><p>digestOf("bug in the bundler")</p>',
813+
);
814+
}
800815

801-
expect(reportedErrors).toEqual([]);
816+
expect(reportedErrors).toEqual(['bug in the bundler']);
802817
});
803818
});

packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,47 @@ export function resolveModuleMetaData<T>(
5757

5858
export type Chunk = RowEncoding;
5959

60-
export function processErrorChunk(
60+
export function processErrorChunkProd(
6161
request: Request,
6262
id: number,
63+
digest: string,
64+
): Chunk {
65+
if (__DEV__) {
66+
// These errors should never make it into a build so we don't need to encode them in codes.json
67+
// eslint-disable-next-line react-internal/prod-error-codes
68+
throw new Error(
69+
'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
70+
);
71+
}
72+
73+
return [
74+
'E',
75+
id,
76+
{
77+
digest,
78+
},
79+
];
80+
}
81+
export function processErrorChunkDev(
82+
request: Request,
83+
id: number,
84+
digest: string,
6385
message: string,
6486
stack: string,
6587
): Chunk {
88+
if (!__DEV__) {
89+
// These errors should never make it into a build so we don't need to encode them in codes.json
90+
// eslint-disable-next-line react-internal/prod-error-codes
91+
throw new Error(
92+
'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
93+
);
94+
}
95+
6696
return [
6797
'E',
6898
id,
6999
{
100+
digest,
70101
message,
71102
stack,
72103
},

0 commit comments

Comments
 (0)