[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:
parent
c1d414d758
commit
efc6a08e98
|
@ -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 {
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
const error = new Error(message);
|
||||
error.stack = 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(
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,9 @@ export type RowEncoding =
|
|||
'E',
|
||||
number,
|
||||
{
|
||||
message: string,
|
||||
stack: string,
|
||||
digest: string,
|
||||
message?: string,
|
||||
stack?: string,
|
||||
...
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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!');
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,9 @@ export type RowEncoding =
|
|||
'E',
|
||||
number,
|
||||
{
|
||||
message: string,
|
||||
stack: string,
|
||||
digest: string,
|
||||
message?: string,
|
||||
stack?: string,
|
||||
...
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue