mirror of
https://github.com/facebook/react.git
synced 2026-02-21 19:31:52 +00:00
[Flight] Add support for transporting Error.cause (#35810)
This commit is contained in:
committed by
GitHub
parent
38cd020c1f
commit
2ba3065527
22
packages/react-client/src/ReactFlightClient.js
vendored
22
packages/react-client/src/ReactFlightClient.js
vendored
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
JSONValue,
|
||||
Thenable,
|
||||
ReactDebugInfo,
|
||||
ReactDebugInfoEntry,
|
||||
@@ -132,14 +133,6 @@ interface FlightStreamController {
|
||||
|
||||
type UninitializedModel = string;
|
||||
|
||||
export type JSONValue =
|
||||
| number
|
||||
| null
|
||||
| boolean
|
||||
| string
|
||||
| {+[key: string]: JSONValue}
|
||||
| $ReadOnlyArray<JSONValue>;
|
||||
|
||||
type ProfilingResult = {
|
||||
track: number,
|
||||
endTime: number,
|
||||
@@ -3527,6 +3520,18 @@ function resolveErrorDev(
|
||||
}
|
||||
|
||||
let error;
|
||||
const errorOptions =
|
||||
'cause' in errorInfo
|
||||
? {
|
||||
cause: reviveModel(
|
||||
response,
|
||||
// $FlowFixMe[incompatible-cast] -- Flow thinks `cause` in `cause?: JSONValue` can be undefined after `in` check.
|
||||
(errorInfo.cause: JSONValue),
|
||||
errorInfo,
|
||||
'cause',
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
@@ -3537,6 +3542,7 @@ function resolveErrorDev(
|
||||
null,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
errorOptions,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -707,6 +707,139 @@ describe('ReactFlight', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('can transport Error.cause', async () => {
|
||||
function renderError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return `${JSON.stringify(error)}`;
|
||||
}
|
||||
return `
|
||||
is error: ${error instanceof Error}
|
||||
name: ${error.name}
|
||||
message: ${error.message}
|
||||
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
|
||||
environmentName: ${error.environmentName}
|
||||
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
|
||||
}
|
||||
function ComponentClient({error}) {
|
||||
return renderError(error);
|
||||
}
|
||||
const Component = clientReference(ComponentClient);
|
||||
|
||||
function ServerComponent() {
|
||||
const cause = new TypeError('root cause', {
|
||||
cause: {type: 'object cause'},
|
||||
});
|
||||
const error = new Error('hello', {cause});
|
||||
return <Component error={error} />;
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
|
||||
onError(x) {
|
||||
if (__DEV__) {
|
||||
return 'a dev digest';
|
||||
}
|
||||
return `digest("${x.message}")`;
|
||||
},
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(ReactNoopFlightClient.read(transport));
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
expect(ReactNoop).toMatchRenderedOutput(`
|
||||
is error: true
|
||||
name: Error
|
||||
message: hello
|
||||
stack: Error: hello
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause:
|
||||
is error: true
|
||||
name: TypeError
|
||||
message: root cause
|
||||
stack: TypeError: root cause
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: {"type":"object cause"}`);
|
||||
} 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
|
||||
cause: no cause`);
|
||||
}
|
||||
});
|
||||
|
||||
it('includes Error.cause in thrown errors', async () => {
|
||||
function renderError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return `${JSON.stringify(error)}`;
|
||||
}
|
||||
return `
|
||||
is error: true
|
||||
name: ${error.name}
|
||||
message: ${error.message}
|
||||
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
|
||||
environmentName: ${error.environmentName}
|
||||
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
|
||||
}
|
||||
|
||||
function ServerComponent() {
|
||||
const cause = new TypeError('root cause', {
|
||||
cause: {type: 'object cause'},
|
||||
});
|
||||
const error = new Error('hello', {cause});
|
||||
throw error;
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
|
||||
onError(x) {
|
||||
if (__DEV__) {
|
||||
return 'a dev digest';
|
||||
}
|
||||
return `digest("${x.message}")`;
|
||||
},
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
await act(() => {
|
||||
ReactNoop.render(ReactNoopFlightClient.read(transport));
|
||||
});
|
||||
} catch (x) {
|
||||
error = x;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(renderError(error)).toEqual(`
|
||||
is error: true
|
||||
name: Error
|
||||
message: hello
|
||||
stack: Error: hello
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause:
|
||||
is error: true
|
||||
name: TypeError
|
||||
message: root cause
|
||||
stack: TypeError: root cause
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: {"type":"object cause"}`);
|
||||
} else {
|
||||
expect(renderError(error)).toEqual(`
|
||||
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
|
||||
cause: no cause`);
|
||||
}
|
||||
});
|
||||
|
||||
it('can transport cyclic objects', async () => {
|
||||
function ComponentClient({prop}) {
|
||||
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
|
||||
|
||||
39
packages/react-server/src/ReactFlightServer.js
vendored
39
packages/react-server/src/ReactFlightServer.js
vendored
@@ -467,14 +467,6 @@ function getCurrentStackInDEV(): string {
|
||||
|
||||
const ObjectPrototype = Object.prototype;
|
||||
|
||||
type JSONValue =
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| null
|
||||
| {+[key: string]: JSONValue}
|
||||
| $ReadOnlyArray<JSONValue>;
|
||||
|
||||
const stringify = JSON.stringify;
|
||||
|
||||
type ReactJSONValue =
|
||||
@@ -498,6 +490,7 @@ export type ReactClientValue =
|
||||
| React$Element<string>
|
||||
| React$Element<ClientReference<any> & any>
|
||||
| ReactComponentInfo
|
||||
| ReactErrorInfo
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
@@ -4171,6 +4164,11 @@ function serializeErrorValue(request: Request, error: Error): string {
|
||||
stack = [];
|
||||
}
|
||||
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
|
||||
if ('cause' in error) {
|
||||
const cause: ReactClientValue = (error.cause: any);
|
||||
const causeId = outlineModel(request, cause);
|
||||
errorInfo.cause = serializeByValueID(causeId);
|
||||
}
|
||||
const id = outlineModel(request, errorInfo);
|
||||
return '$Z' + id.toString(16);
|
||||
} else {
|
||||
@@ -4181,7 +4179,11 @@ function serializeErrorValue(request: Request, error: Error): string {
|
||||
}
|
||||
}
|
||||
|
||||
function serializeDebugErrorValue(request: Request, error: Error): string {
|
||||
function serializeDebugErrorValue(
|
||||
request: Request,
|
||||
counter: {objectLimit: number},
|
||||
error: Error,
|
||||
): string {
|
||||
if (__DEV__) {
|
||||
let name: string = 'Error';
|
||||
let message: string;
|
||||
@@ -4203,6 +4205,12 @@ function serializeDebugErrorValue(request: Request, error: Error): string {
|
||||
stack = [];
|
||||
}
|
||||
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
|
||||
if ('cause' in error) {
|
||||
counter.objectLimit--;
|
||||
const cause: ReactClientValue = (error.cause: any);
|
||||
const causeId = outlineDebugModel(request, counter, cause);
|
||||
errorInfo.cause = serializeByValueID(causeId);
|
||||
}
|
||||
const id = outlineDebugModel(
|
||||
request,
|
||||
{objectLimit: stack.length * 2 + 1},
|
||||
@@ -4231,6 +4239,7 @@ function emitErrorChunk(
|
||||
let message: string;
|
||||
let stack: ReactStackTrace;
|
||||
let env = (0, request.environmentName)();
|
||||
let causeReference: null | string = null;
|
||||
try {
|
||||
if (error instanceof Error) {
|
||||
name = error.name;
|
||||
@@ -4243,6 +4252,13 @@ function emitErrorChunk(
|
||||
// Keep the environment name.
|
||||
env = errorEnv;
|
||||
}
|
||||
if ('cause' in error) {
|
||||
const cause: ReactClientValue = (error.cause: any);
|
||||
const causeId = debug
|
||||
? outlineDebugModel(request, {objectLimit: 5}, cause)
|
||||
: outlineModel(request, cause);
|
||||
causeReference = serializeByValueID(causeId);
|
||||
}
|
||||
} else if (typeof error === 'object' && error !== null) {
|
||||
message = describeObjectForErrorMessage(error);
|
||||
stack = [];
|
||||
@@ -4258,6 +4274,9 @@ function emitErrorChunk(
|
||||
const ownerRef =
|
||||
owner == null ? null : outlineComponentInfo(request, owner);
|
||||
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
|
||||
if (causeReference !== null) {
|
||||
(errorInfo: ReactErrorInfoDev).cause = causeReference;
|
||||
}
|
||||
} else {
|
||||
errorInfo = {digest};
|
||||
}
|
||||
@@ -4969,7 +4988,7 @@ function renderDebugModel(
|
||||
return serializeDebugFormData(request, value);
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
return serializeDebugErrorValue(request, value);
|
||||
return serializeDebugErrorValue(request, counter, value);
|
||||
}
|
||||
if (value instanceof ArrayBuffer) {
|
||||
return serializeDebugTypedArray(request, 'A', new Uint8Array(value));
|
||||
|
||||
@@ -228,6 +228,14 @@ export type ReactErrorInfoProd = {
|
||||
+digest: string,
|
||||
};
|
||||
|
||||
export type JSONValue =
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| null
|
||||
| {+[key: string]: JSONValue}
|
||||
| $ReadOnlyArray<JSONValue>;
|
||||
|
||||
export type ReactErrorInfoDev = {
|
||||
+digest?: string,
|
||||
+name: string,
|
||||
@@ -235,6 +243,7 @@ export type ReactErrorInfoDev = {
|
||||
+stack: ReactStackTrace,
|
||||
+env: string,
|
||||
+owner?: null | string,
|
||||
cause?: JSONValue,
|
||||
};
|
||||
|
||||
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
|
||||
|
||||
Reference in New Issue
Block a user