diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 1dbe46ebce1c8..44815dce4b5b9 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -193,7 +193,7 @@ function createBlockedChunk(response: Response): BlockedChunk {
function createErrorChunk(
response: Response,
- error: Error,
+ error: ErrorWithDigest,
): ErroredChunk {
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(ERRORED, null, error, response);
@@ -628,21 +628,64 @@ export function resolveSymbol(
chunks.set(id, createInitializedChunk(response, Symbol.for(name)));
}
-export function resolveError(
+type ErrorWithDigest = Error & {digest?: string};
+export function resolveErrorProd(
response: Response,
id: number,
+ digest: string,
+): void {
+ if (__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.',
+ );
+ }
+ const error = new Error(
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
+ ' may provide additional details about the nature of the error.',
+ );
+ error.stack = '';
+ (error: any).digest = digest;
+ const errorWithDigest: ErrorWithDigest = (error: any);
+ const chunks = response._chunks;
+ const chunk = chunks.get(id);
+ if (!chunk) {
+ chunks.set(id, createErrorChunk(response, errorWithDigest));
+ } else {
+ triggerErrorOnChunk(chunk, errorWithDigest);
+ }
+}
+
+export function resolveErrorDev(
+ response: Response,
+ id: number,
+ digest: string,
message: string,
stack: string,
): void {
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
+ );
+ }
// eslint-disable-next-line react-internal/prod-error-codes
- const error = new Error(message);
+ const error = new Error(
+ message ||
+ 'An error occurred in the Server Components render but no message was provided',
+ );
error.stack = stack;
+ (error: any).digest = digest;
+ const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
- chunks.set(id, createErrorChunk(response, error));
+ chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
- triggerErrorOnChunk(chunk, error);
+ triggerErrorOnChunk(chunk, errorWithDigest);
}
}
diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js
index b6c61b0ba5de7..c5bf302a36160 100644
--- a/packages/react-client/src/ReactFlightClientStream.js
+++ b/packages/react-client/src/ReactFlightClientStream.js
@@ -16,7 +16,8 @@ import {
resolveModel,
resolveProvider,
resolveSymbol,
- resolveError,
+ resolveErrorProd,
+ resolveErrorDev,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
@@ -62,7 +63,17 @@ function processFullRow(response: Response, row: string): void {
}
case 'E': {
const errorInfo = JSON.parse(text);
- resolveError(response, id, errorInfo.message, errorInfo.stack);
+ if (__DEV__) {
+ resolveErrorDev(
+ response,
+ id,
+ errorInfo.digest,
+ errorInfo.message,
+ errorInfo.stack,
+ );
+ } else {
+ resolveErrorProd(response, id, errorInfo.digest);
+ }
return;
}
default: {
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index b03a7087656aa..4b72bdd85a4e1 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -45,7 +45,19 @@ describe('ReactFlight', () => {
componentDidMount() {
expect(this.state.hasError).toBe(true);
expect(this.state.error).toBeTruthy();
- expect(this.state.error.message).toContain(this.props.expectedMessage);
+ if (__DEV__) {
+ expect(this.state.error.message).toContain(
+ this.props.expectedMessage,
+ );
+ expect(this.state.error.digest).toBe('a dev digest');
+ } else {
+ expect(this.state.error.message).toBe(
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
+ ' may provide additional details about the nature of the error.',
+ );
+ expect(this.state.error.digest).toContain(this.props.expectedMessage);
+ }
}
render() {
if (this.state.hasError) {
@@ -371,8 +383,8 @@ describe('ReactFlight', () => {
}
const options = {
- onError() {
- // ignore
+ onError(x) {
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
};
const event = ReactNoopFlightServer.render(, options);
diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
index ecee74598c579..85050f6558898 100644
--- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
+++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
@@ -16,7 +16,8 @@ import {
resolveModel,
resolveModule,
resolveSymbol,
- resolveError,
+ resolveErrorDev,
+ resolveErrorProd,
close,
getRoot,
} from 'react-client/src/ReactFlightClient';
@@ -34,7 +35,20 @@ export function resolveRow(response: Response, chunk: RowEncoding): void {
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveSymbol(response, chunk[1], chunk[2]);
} else {
- // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
- resolveError(response, chunk[1], chunk[2].message, chunk[2].stack);
+ if (__DEV__) {
+ resolveErrorDev(
+ response,
+ chunk[1],
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].digest,
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].message || '',
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].stack || '',
+ );
+ } else {
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ resolveErrorProd(response, chunk[1], chunk[2].digest);
+ }
}
}
diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
index d883068c9eaa7..ad6c48b2ebc4f 100644
--- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
+++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
@@ -26,8 +26,9 @@ export type RowEncoding =
'E',
number,
{
- message: string,
- stack: string,
+ digest: string,
+ message?: string,
+ stack?: string,
...
},
];
diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
index 35a1b3e16d552..fc205a445013f 100644
--- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
@@ -60,16 +60,48 @@ export function resolveModuleMetaData(
export type Chunk = RowEncoding;
-export function processErrorChunk(
+export function processErrorChunkProd(
request: Request,
id: number,
+ digest: string,
+): Chunk {
+ if (__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
+ );
+ }
+
+ return [
+ 'E',
+ id,
+ {
+ digest,
+ },
+ ];
+}
+
+export function processErrorChunkDev(
+ request: Request,
+ id: number,
+ digest: string,
message: string,
stack: string,
): Chunk {
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
+ );
+ }
+
return [
'E',
id,
{
+ digest,
message,
stack,
},
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index f8948c3604a58..4a835834152f3 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -332,7 +332,13 @@ describe('ReactFlightDOM', () => {
function MyErrorBoundary({children}) {
return (
- {e.message}
}>
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
{children}
);
@@ -434,6 +440,7 @@ describe('ReactFlightDOM', () => {
{
onError(x) {
reportedErrors.push(x);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
},
);
@@ -477,11 +484,14 @@ describe('ReactFlightDOM', () => {
await act(async () => {
rejectGames(theError);
});
+ const expectedGamesValue = __DEV__
+ ? 'Game over + a dev digest
'
+ : 'digest("Game over")
';
expect(container.innerHTML).toBe(
':name::avatar:
' +
'(loading sidebar)
' +
'(loading posts)
' +
- 'Game over
', // TODO: should not have message in prod.
+ expectedGamesValue,
);
expect(reportedErrors).toEqual([theError]);
@@ -495,7 +505,7 @@ describe('ReactFlightDOM', () => {
':name::avatar:
' +
':photos::friends:
' +
'(loading posts)
' +
- 'Game over
', // TODO: should not have message in prod.
+ expectedGamesValue,
);
// Show everything.
@@ -506,7 +516,7 @@ describe('ReactFlightDOM', () => {
':name::avatar:
' +
':photos::friends:
' +
':posts:
' +
- 'Game over
', // TODO: should not have message in prod.
+ expectedGamesValue,
);
expect(reportedErrors).toEqual([]);
@@ -611,6 +621,8 @@ describe('ReactFlightDOM', () => {
{
onError(x) {
reportedErrors.push(x);
+ const message = typeof x === 'string' ? x : x.message;
+ return __DEV__ ? 'a dev digest' : `digest("${message}")`;
},
},
);
@@ -626,7 +638,13 @@ describe('ReactFlightDOM', () => {
await act(async () => {
root.render(
- {e.message}
}>
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
(loading)
}>
@@ -638,7 +656,13 @@ describe('ReactFlightDOM', () => {
await act(async () => {
abort('for reasons');
});
- expect(container.innerHTML).toBe('Error: for reasons
');
+ if (__DEV__) {
+ expect(container.innerHTML).toBe(
+ 'Error: for reasons + a dev digest
',
+ );
+ } else {
+ expect(container.innerHTML).toBe('digest("for reasons")
');
+ }
expect(reportedErrors).toEqual(['for reasons']);
});
@@ -772,7 +796,8 @@ describe('ReactFlightDOM', () => {
webpackMap,
{
onError(x) {
- reportedErrors.push(x);
+ reportedErrors.push(x.message);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
},
);
@@ -789,15 +814,27 @@ describe('ReactFlightDOM', () => {
await act(async () => {
root.render(
- {e.message}
}>
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
(loading)}>
,
);
});
- expect(container.innerHTML).toBe('bug in the bundler
');
+ if (__DEV__) {
+ expect(container.innerHTML).toBe(
+ 'bug in the bundler + a dev digest
',
+ );
+ } else {
+ expect(container.innerHTML).toBe('digest("bug in the bundler")
');
+ }
- expect(reportedErrors).toEqual([]);
+ expect(reportedErrors).toEqual(['bug in the bundler']);
});
});
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
index 7ac0a2b4aa9d1..2ff81ce2e843d 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -173,11 +173,27 @@ describe('ReactFlightDOMBrowser', () => {
}
}
+ let errorBoundaryFn;
+ if (__DEV__) {
+ errorBoundaryFn = e => (
+
+ {e.message} + {e.digest}
+
+ );
+ } else {
+ errorBoundaryFn = e => {
+ expect(e.message).toBe(
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
+ ' may provide additional details about the nature of the error.',
+ );
+ return {e.digest}
;
+ };
+ }
+
function MyErrorBoundary({children}) {
return (
- {e.message}
}>
- {children}
-
+ {children}
);
}
@@ -251,6 +267,7 @@ describe('ReactFlightDOMBrowser', () => {
{
onError(x) {
reportedErrors.push(x);
+ return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
},
},
);
@@ -293,11 +310,16 @@ describe('ReactFlightDOMBrowser', () => {
await act(async () => {
rejectGames(theError);
});
+
+ const gamesExpectedValue = __DEV__
+ ? 'Game over + a dev digest
'
+ : 'digest("Game over")
';
+
expect(container.innerHTML).toBe(
':name::avatar:
' +
'(loading sidebar)
' +
'(loading posts)
' +
- 'Game over
', // TODO: should not have message in prod.
+ gamesExpectedValue,
);
expect(reportedErrors).toEqual([theError]);
@@ -311,7 +333,7 @@ describe('ReactFlightDOMBrowser', () => {
':name::avatar:
' +
':photos::friends:
' +
'(loading posts)
' +
- 'Game over
', // TODO: should not have message in prod.
+ gamesExpectedValue,
);
// Show everything.
@@ -322,7 +344,7 @@ describe('ReactFlightDOMBrowser', () => {
':name::avatar:
' +
':photos::friends:
' +
':posts:
' +
- 'Game over
', // TODO: should not have message in prod.
+ gamesExpectedValue,
);
expect(reportedErrors).toEqual([]);
@@ -489,6 +511,24 @@ describe('ReactFlightDOMBrowser', () => {
it('should be able to complete after aborting and throw the reason client-side', async () => {
const reportedErrors = [];
+ let errorBoundaryFn;
+ if (__DEV__) {
+ errorBoundaryFn = e => (
+
+ {e.message} + {e.digest}
+
+ );
+ } else {
+ errorBoundaryFn = e => {
+ expect(e.message).toBe(
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
+ ' may provide additional details about the nature of the error.',
+ );
+ return {e.digest}
;
+ };
+ }
+
class ErrorBoundary extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
@@ -514,7 +554,9 @@ describe('ReactFlightDOMBrowser', () => {
{
signal: controller.signal,
onError(x) {
+ const message = typeof x === 'string' ? x : x.message;
reportedErrors.push(x);
+ return __DEV__ ? 'a dev digest' : `digest("${message}")`;
},
},
);
@@ -529,7 +571,7 @@ describe('ReactFlightDOMBrowser', () => {
await act(async () => {
root.render(
- {e.message}
}>
+
(loading)}>
@@ -545,7 +587,10 @@ describe('ReactFlightDOMBrowser', () => {
controller.signal.reason = 'for reasons';
controller.abort('for reasons');
});
- expect(container.innerHTML).toBe('Error: for reasons
');
+ const expectedValue = __DEV__
+ ? 'Error: for reasons + a dev digest
'
+ : 'digest("for reasons")
';
+ expect(container.innerHTML).toBe(expectedValue);
expect(reportedErrors).toEqual(['for reasons']);
});
@@ -665,6 +710,7 @@ describe('ReactFlightDOMBrowser', () => {
{
onError(x) {
reportedErrors.push(x);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
},
);
@@ -677,7 +723,9 @@ describe('ReactFlightDOMBrowser', () => {
}
render() {
if (this.state.error) {
- return this.state.error.message;
+ return __DEV__
+ ? this.state.error.message + ' + ' + this.state.error.digest
+ : this.state.error.digest;
}
return this.props.children;
}
@@ -696,7 +744,9 @@ describe('ReactFlightDOMBrowser', () => {
,
);
});
- expect(container.innerHTML).toBe('Oops!');
+ expect(container.innerHTML).toBe(
+ __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
+ );
expect(reportedErrors.length).toBe(1);
expect(reportedErrors[0].message).toBe('Oops!');
});
diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js
index 087b93479d424..030040e404846 100644
--- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js
+++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js
@@ -16,7 +16,8 @@ import {
resolveModel,
resolveModule,
resolveSymbol,
- resolveError,
+ resolveErrorDev,
+ resolveErrorProd,
close,
getRoot,
} from 'react-client/src/ReactFlightClient';
@@ -34,7 +35,20 @@ export function resolveRow(response: Response, chunk: RowEncoding): void {
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveSymbol(response, chunk[1], chunk[2]);
} else {
- // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
- resolveError(response, chunk[1], chunk[2].message, chunk[2].stack);
+ if (__DEV__) {
+ resolveErrorDev(
+ response,
+ chunk[1],
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].digest,
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].message || '',
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ chunk[2].stack || '',
+ );
+ } else {
+ // $FlowFixMe: Flow doesn't support disjoint unions on tuples.
+ resolveErrorProd(response, chunk[1], chunk[2].digest);
+ }
}
}
diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js
index 1c32ac0dd4d44..c161cccefd2f1 100644
--- a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js
+++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js
@@ -26,8 +26,9 @@ export type RowEncoding =
'E',
number,
{
- message: string,
- stack: string,
+ digest: string,
+ message?: string,
+ stack?: string,
...
},
];
diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
index 65d60ec334ea1..e1817cebf53f6 100644
--- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
+++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js
@@ -57,16 +57,47 @@ export function resolveModuleMetaData(
export type Chunk = RowEncoding;
-export function processErrorChunk(
+export function processErrorChunkProd(
request: Request,
id: number,
+ digest: string,
+): Chunk {
+ if (__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
+ );
+ }
+
+ return [
+ 'E',
+ id,
+ {
+ digest,
+ },
+ ];
+}
+export function processErrorChunkDev(
+ request: Request,
+ id: number,
+ digest: string,
message: string,
stack: string,
): Chunk {
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
+ );
+ }
+
return [
'E',
id,
{
+ digest,
message,
stack,
},
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 6f26f8b88f693..67014934e429d 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -35,7 +35,8 @@ import {
processModuleChunk,
processProviderChunk,
processSymbolChunk,
- processErrorChunk,
+ processErrorChunkProd,
+ processErrorChunkDev,
processReferenceChunk,
resolveModuleMetaData,
getModuleKey,
@@ -125,7 +126,7 @@ export type Request = {
writtenProviders: Map,
identifierPrefix: string,
identifierCount: number,
- onError: (error: mixed) => void,
+ onError: (error: mixed) => ?string,
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
};
@@ -143,7 +144,7 @@ const CLOSED = 2;
export function createRequest(
model: ReactModel,
bundlerConfig: BundlerConfig,
- onError: void | ((error: mixed) => void),
+ onError: void | ((error: mixed) => ?string),
context?: Array<[string, ServerContextJSONValue]>,
identifierPrefix?: string,
): Request {
@@ -364,7 +365,13 @@ function serializeModuleReference(
} catch (x) {
request.pendingChunks++;
const errorId = request.nextChunkId++;
- emitErrorChunk(request, errorId, x);
+ const digest = logRecoverableError(request, x);
+ if (__DEV__) {
+ const {message, stack} = getErrorMessageAndStackDev(x);
+ emitErrorChunkDev(request, errorId, digest, message, stack);
+ } else {
+ emitErrorChunkProd(request, errorId, digest);
+ }
return serializeByValueID(errorId);
}
}
@@ -629,7 +636,13 @@ export function resolveModelToJSON(
// once it gets rendered.
request.pendingChunks++;
const errorId = request.nextChunkId++;
- emitErrorChunk(request, errorId, x);
+ const digest = logRecoverableError(request, x);
+ if (__DEV__) {
+ const {message, stack} = getErrorMessageAndStackDev(x);
+ emitErrorChunkDev(request, errorId, digest, message, stack);
+ } else {
+ emitErrorChunkProd(request, errorId, digest);
+ }
return serializeByRefID(errorId);
}
}
@@ -797,9 +810,47 @@ export function resolveModelToJSON(
);
}
-function logRecoverableError(request: Request, error: mixed): void {
+function logRecoverableError(request: Request, error: mixed): string {
const onError = request.onError;
- onError(error);
+ const errorDigest = onError(error);
+ if (errorDigest != null && typeof errorDigest !== 'string') {
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ `onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorDigest}" instead`,
+ );
+ }
+ return errorDigest || '';
+}
+
+function getErrorMessageAndStackDev(
+ error: mixed,
+): {message: string, stack: string} {
+ if (__DEV__) {
+ let message;
+ let stack = '';
+ try {
+ if (error instanceof Error) {
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ message = String(error.message);
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ stack = String(error.stack);
+ } else {
+ message = 'Error: ' + (error: any);
+ }
+ } catch (x) {
+ message = 'An error occurred but serializing the error message failed.';
+ }
+ return {
+ message,
+ stack,
+ };
+ } else {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'getErrorMessageAndStackDev should never be called from production mode. This is a bug in React.',
+ );
+ }
}
function fatalError(request: Request, error: mixed): void {
@@ -813,26 +864,29 @@ function fatalError(request: Request, error: mixed): void {
}
}
-function emitErrorChunk(request: Request, id: number, error: mixed): void {
- // TODO: We should not leak error messages to the client in prod.
- // Give this an error code instead and log on the server.
- // We can serialize the error in DEV as a convenience.
- let message;
- let stack = '';
- try {
- if (error instanceof Error) {
- // eslint-disable-next-line react-internal/safe-string-coercion
- message = String(error.message);
- // eslint-disable-next-line react-internal/safe-string-coercion
- stack = String(error.stack);
- } else {
- message = 'Error: ' + (error: any);
- }
- } catch (x) {
- message = 'An error occurred but serializing the error message failed.';
- }
+function emitErrorChunkProd(
+ request: Request,
+ id: number,
+ digest: string,
+): void {
+ const processedChunk = processErrorChunkProd(request, id, digest);
+ request.completedErrorChunks.push(processedChunk);
+}
- const processedChunk = processErrorChunk(request, id, message, stack);
+function emitErrorChunkDev(
+ request: Request,
+ id: number,
+ digest: string,
+ message: string,
+ stack: string,
+): void {
+ const processedChunk = processErrorChunkDev(
+ request,
+ id,
+ digest,
+ message,
+ stack,
+ );
request.completedErrorChunks.push(processedChunk);
}
@@ -935,9 +989,13 @@ function retryTask(request: Request, task: Task): void {
} else {
request.abortableTasks.delete(task);
task.status = ERRORED;
- logRecoverableError(request, x);
- // This errored, we need to serialize this error to the
- emitErrorChunk(request, task.id, x);
+ const digest = logRecoverableError(request, x);
+ if (__DEV__) {
+ const {message, stack} = getErrorMessageAndStackDev(x);
+ emitErrorChunkDev(request, task.id, digest, message, stack);
+ } else {
+ emitErrorChunkProd(request, task.id, digest);
+ }
}
}
}
@@ -1077,10 +1135,15 @@ export function abort(request: Request, reason: mixed): void {
? new Error('The render was aborted by the server without a reason.')
: reason;
- logRecoverableError(request, error);
+ const digest = logRecoverableError(request, error);
request.pendingChunks++;
const errorId = request.nextChunkId++;
- emitErrorChunk(request, errorId, error);
+ if (__DEV__) {
+ const {message, stack} = getErrorMessageAndStackDev(error);
+ emitErrorChunkDev(request, errorId, digest, message, stack);
+ } else {
+ emitErrorChunkProd(request, errorId, digest);
+ }
abortableTasks.forEach(task => abortTask(task, request, errorId));
abortableTasks.clear();
}
diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js
index 6320f28d585b6..7944137288eb5 100644
--- a/packages/react-server/src/ReactFlightServerConfigStream.js
+++ b/packages/react-server/src/ReactFlightServerConfigStream.js
@@ -78,13 +78,40 @@ function serializeRowHeader(tag: string, id: number) {
return tag + id.toString(16) + ':';
}
-export function processErrorChunk(
+export function processErrorChunkProd(
request: Request,
id: number,
+ digest: string,
+): Chunk {
+ if (__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
+ );
+ }
+
+ const errorInfo: any = {digest};
+ const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
+ return stringToChunk(row);
+}
+
+export function processErrorChunkDev(
+ request: Request,
+ id: number,
+ digest: string,
message: string,
stack: string,
): Chunk {
- const errorInfo = {message, stack};
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
+ );
+ }
+
+ const errorInfo: any = {digest, message, stack};
const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
return stringToChunk(row);
}
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 7a23215cc332a..4b50cbdcfcf44 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -425,5 +425,6 @@
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
"438": "An unsupported type was passed to use(): %s",
"439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
- "440": "An event from useEvent was called during render."
+ "440": "An event from useEvent was called during render.",
+ "441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."
}