[Flight] Implement error digests for Flight runtime and expose errorInfo in getDerivedStateFromError (#25302)

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.

The digest is stashed on the constructed Error on the client as .digest
This commit is contained in:
Josh Story 2022-09-23 13:19:29 -07:00 committed by GitHub
parent c1d414d758
commit efc6a08e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 421 additions and 84 deletions

View File

@ -193,7 +193,7 @@ function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
function createErrorChunk<T>(
response: Response,
error: Error,
error: ErrorWithDigest,
): ErroredChunk<T> {
// $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,
message: string,
stack: string,
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
const error = new Error(message);
error.stack = stack;
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, error));
chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
triggerErrorOnChunk(chunk, error);
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 ||
'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, errorWithDigest));
} else {
triggerErrorOnChunk(chunk, errorWithDigest);
}
}

View File

@ -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: {

View File

@ -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(<EventHandlerProp />, options);

View File

@ -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 {
if (__DEV__) {
resolveErrorDev(
response,
chunk[1],
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveError(response, chunk[1], chunk[2].message, chunk[2].stack);
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);
}
}
}

View File

@ -26,8 +26,9 @@ export type RowEncoding =
'E',
number,
{
message: string,
stack: string,
digest: string,
message?: string,
stack?: string,
...
},
];

View File

@ -60,16 +60,48 @@ export function resolveModuleMetaData<T>(
export type Chunk = RowEncoding;
export function processErrorChunk(
export function processErrorChunkProd(
request: Request,
id: number,
message: string,
stack: string,
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,
},

View File

@ -332,7 +332,13 @@ describe('ReactFlightDOM', () => {
function MyErrorBoundary({children}) {
return (
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
{children}
</ErrorBoundary>
);
@ -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__
? '<p>Game over + a dev digest</p>'
: '<p>digest("Game over")</p>';
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
'<p>(loading posts)</p>' +
'<p>Game over</p>', // TODO: should not have message in prod.
expectedGamesValue,
);
expect(reportedErrors).toEqual([theError]);
@ -495,7 +505,7 @@ describe('ReactFlightDOM', () => {
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<p>(loading posts)</p>' +
'<p>Game over</p>', // TODO: should not have message in prod.
expectedGamesValue,
);
// Show everything.
@ -506,7 +516,7 @@ describe('ReactFlightDOM', () => {
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<div>:posts:</div>' +
'<p>Game over</p>', // 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(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
@ -638,7 +656,13 @@ describe('ReactFlightDOM', () => {
await act(async () => {
abort('for reasons');
});
expect(container.innerHTML).toBe('<p>Error: for reasons</p>');
if (__DEV__) {
expect(container.innerHTML).toBe(
'<p>Error: for reasons + a dev digest</p>',
);
} else {
expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
}
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(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<ErrorBoundary
fallback={e => (
<p>
{__DEV__ ? e.message + ' + ' : null}
{e.digest}
</p>
)}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>bug in the bundler</p>');
if (__DEV__) {
expect(container.innerHTML).toBe(
'<p>bug in the bundler + a dev digest</p>',
);
} else {
expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
}
expect(reportedErrors).toEqual([]);
expect(reportedErrors).toEqual(['bug in the bundler']);
});
});

View File

@ -173,11 +173,27 @@ describe('ReactFlightDOMBrowser', () => {
}
}
let errorBoundaryFn;
if (__DEV__) {
errorBoundaryFn = e => (
<p>
{e.message} + {e.digest}
</p>
);
} 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 <p>{e.digest}</p>;
};
}
function MyErrorBoundary({children}) {
return (
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
{children}
</ErrorBoundary>
<ErrorBoundary fallback={errorBoundaryFn}>{children}</ErrorBoundary>
);
}
@ -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__
? '<p>Game over + a dev digest</p>'
: '<p>digest("Game over")</p>';
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
'<p>(loading posts)</p>' +
'<p>Game over</p>', // TODO: should not have message in prod.
gamesExpectedValue,
);
expect(reportedErrors).toEqual([theError]);
@ -311,7 +333,7 @@ describe('ReactFlightDOMBrowser', () => {
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<p>(loading posts)</p>' +
'<p>Game over</p>', // TODO: should not have message in prod.
gamesExpectedValue,
);
// Show everything.
@ -322,7 +344,7 @@ describe('ReactFlightDOMBrowser', () => {
'<div>:name::avatar:</div>' +
'<div>:photos::friends:</div>' +
'<div>:posts:</div>' +
'<p>Game over</p>', // 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 => (
<p>
{e.message} + {e.digest}
</p>
);
} 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 <p>{e.digest}</p>;
};
}
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(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<ErrorBoundary fallback={errorBoundaryFn}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
@ -545,7 +587,10 @@ describe('ReactFlightDOMBrowser', () => {
controller.signal.reason = 'for reasons';
controller.abort('for reasons');
});
expect(container.innerHTML).toBe('<p>Error: for reasons</p>');
const expectedValue = __DEV__
? '<p>Error: for reasons + a dev digest</p>'
: '<p>digest("for reasons")</p>';
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', () => {
</ErrorBoundary>,
);
});
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!');
});

View File

@ -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 {
if (__DEV__) {
resolveErrorDev(
response,
chunk[1],
// $FlowFixMe: Flow doesn't support disjoint unions on tuples.
resolveError(response, chunk[1], chunk[2].message, chunk[2].stack);
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);
}
}
}

View File

@ -26,8 +26,9 @@ export type RowEncoding =
'E',
number,
{
message: string,
stack: string,
digest: string,
message?: string,
stack?: string,
...
},
];

View File

@ -57,16 +57,47 @@ export function resolveModuleMetaData<T>(
export type Chunk = RowEncoding;
export function processErrorChunk(
export function processErrorChunkProd(
request: Request,
id: number,
message: string,
stack: string,
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,
},

View File

@ -35,7 +35,8 @@ import {
processModuleChunk,
processProviderChunk,
processSymbolChunk,
processErrorChunk,
processErrorChunkProd,
processErrorChunkDev,
processReferenceChunk,
resolveModuleMetaData,
getModuleKey,
@ -125,7 +126,7 @@ export type Request = {
writtenProviders: Map<string, number>,
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,26 +810,22 @@ export function resolveModelToJSON(
);
}
function logRecoverableError(request: Request, error: mixed): void {
function logRecoverableError(request: Request, error: mixed): string {
const onError = request.onError;
onError(error);
}
function fatalError(request: Request, error: mixed): void {
// This is called outside error handling code such as if an error happens in React internals.
if (request.destination !== null) {
request.status = CLOSED;
closeWithError(request.destination, error);
} else {
request.status = CLOSING;
request.fatalError = 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 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.
function getErrorMessageAndStackDev(
error: mixed,
): {message: string, stack: string} {
if (__DEV__) {
let message;
let stack = '';
try {
@ -831,8 +840,53 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void {
} 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.',
);
}
}
const processedChunk = processErrorChunk(request, id, message, stack);
function fatalError(request: Request, error: mixed): void {
// This is called outside error handling code such as if an error happens in React internals.
if (request.destination !== null) {
request.status = CLOSED;
closeWithError(request.destination, error);
} else {
request.status = CLOSING;
request.fatalError = error;
}
}
function emitErrorChunkProd(
request: Request,
id: number,
digest: string,
): void {
const processedChunk = processErrorChunkProd(request, id, digest);
request.completedErrorChunks.push(processedChunk);
}
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();
}

View File

@ -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);
}

View File

@ -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."
}