From 829401dc173d79994a3401fce24084670f55fb5c Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 17 Jan 2025 23:48:57 +0100 Subject: [PATCH] [Flight] Transport custom error names in dev mode (#32116) Typed errors is not a feature that Flight currently supports. However, for presentation purposes, serializing a custom error name is something we could support today. With this PR, we're now transporting custom error names through the server-client boundary, so that they are available in the client e.g. for console replaying. One example where this can be useful is when you want to print debug information while leveraging the fact that `console.warn` displays the error stack, including handling of hiding and source mapping stack frames. In this case you may want to show `Warning: ...` or `Debug: ...` instead of `Error: ...`. In prod mode, we still transport an obfuscated error that uses the default `Error` name, to not leak any sensitive information from the server to the client. This also means that you must not rely on the error name to discriminate errors, e.g. when handling them in an error boundary. --- packages/react-client/src/ReactFlightClient.js | 10 +++++++++- .../react-client/src/__tests__/ReactFlight-test.js | 14 ++++++++++++-- packages/react-server/src/ReactFlightServer.js | 4 +++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 288d6b7c1d018..4ea66dcddd742 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2123,8 +2123,15 @@ function resolveErrorProd(response: Response): Error { function resolveErrorDev( response: Response, - errorInfo: {message: string, stack: ReactStackTrace, env: string, ...}, + errorInfo: { + name: string, + message: string, + stack: ReactStackTrace, + env: string, + ... + }, ): Error { + const name: string = errorInfo.name; const message: string = errorInfo.message; const stack: ReactStackTrace = errorInfo.stack; const env: string = errorInfo.env; @@ -2156,6 +2163,7 @@ function resolveErrorDev( error = callStack(); } + (error: any).name = name; (error: any).environmentName = env; return error; } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index e11c11261406f..44b0c76c3f4fb 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -694,9 +694,17 @@ describe('ReactFlight', () => { }); it('can transport Error objects as values', async () => { + class CustomError extends Error { + constructor(message) { + super(message); + this.name = 'Custom'; + } + } + function ComponentClient({prop}) { return ` is error: ${prop instanceof Error} + name: ${prop.name} message: ${prop.message} stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')} environmentName: ${prop.environmentName} @@ -705,7 +713,7 @@ describe('ReactFlight', () => { const Component = clientReference(ComponentClient); function ServerComponent() { - const error = new Error('hello'); + const error = new CustomError('hello'); return ; } @@ -718,14 +726,16 @@ describe('ReactFlight', () => { if (__DEV__) { expect(ReactNoop).toMatchRenderedOutput(` is error: true + name: Custom message: hello - stack: Error: hello + stack: Custom: hello in ServerComponent (at **) environmentName: Server `); } else { expect(ReactNoop).toMatchRenderedOutput(` is error: true + name: Error message: 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. stack: 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. environmentName: undefined diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 67ae695ca0230..cda88fc5c1718 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -3093,10 +3093,12 @@ function emitPostponeChunk( function serializeErrorValue(request: Request, error: Error): string { if (__DEV__) { + let name; let message; let stack: ReactStackTrace; let env = (0, request.environmentName)(); try { + name = error.name; // eslint-disable-next-line react-internal/safe-string-coercion message = String(error.message); stack = filterStackTrace(request, error, 0); @@ -3110,7 +3112,7 @@ function serializeErrorValue(request: Request, error: Error): string { message = 'An error occurred but serializing the error message failed.'; stack = []; } - const errorInfo = {message, stack, env}; + const errorInfo = {name, message, stack, env}; const id = outlineModel(request, errorInfo); return '$Z' + id.toString(16); } else {