From b24436b5c26ba08d54236272d0a10a04c7bf1622 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 17 Jan 2025 22:06:10 +0100 Subject: [PATCH] [Flight] Transport custom error names in dev mode Typed errors is not something 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 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 288d6b7c1d01..4ea66dcddd74 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 e11c11261406..44b0c76c3f4f 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 67ae695ca023..cda88fc5c171 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 {