mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +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 {
|
import type {
|
||||||
|
JSONValue,
|
||||||
Thenable,
|
Thenable,
|
||||||
ReactDebugInfo,
|
ReactDebugInfo,
|
||||||
ReactDebugInfoEntry,
|
ReactDebugInfoEntry,
|
||||||
@@ -132,14 +133,6 @@ interface FlightStreamController {
|
|||||||
|
|
||||||
type UninitializedModel = string;
|
type UninitializedModel = string;
|
||||||
|
|
||||||
export type JSONValue =
|
|
||||||
| number
|
|
||||||
| null
|
|
||||||
| boolean
|
|
||||||
| string
|
|
||||||
| {+[key: string]: JSONValue}
|
|
||||||
| $ReadOnlyArray<JSONValue>;
|
|
||||||
|
|
||||||
type ProfilingResult = {
|
type ProfilingResult = {
|
||||||
track: number,
|
track: number,
|
||||||
endTime: number,
|
endTime: number,
|
||||||
@@ -3527,6 +3520,18 @@ function resolveErrorDev(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let error;
|
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(
|
const callStack = buildFakeCallStack(
|
||||||
response,
|
response,
|
||||||
stack,
|
stack,
|
||||||
@@ -3537,6 +3542,7 @@ function resolveErrorDev(
|
|||||||
null,
|
null,
|
||||||
message ||
|
message ||
|
||||||
'An error occurred in the Server Components render but no message was provided',
|
'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 () => {
|
it('can transport cyclic objects', async () => {
|
||||||
function ComponentClient({prop}) {
|
function ComponentClient({prop}) {
|
||||||
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
|
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;
|
const ObjectPrototype = Object.prototype;
|
||||||
|
|
||||||
type JSONValue =
|
|
||||||
| string
|
|
||||||
| boolean
|
|
||||||
| number
|
|
||||||
| null
|
|
||||||
| {+[key: string]: JSONValue}
|
|
||||||
| $ReadOnlyArray<JSONValue>;
|
|
||||||
|
|
||||||
const stringify = JSON.stringify;
|
const stringify = JSON.stringify;
|
||||||
|
|
||||||
type ReactJSONValue =
|
type ReactJSONValue =
|
||||||
@@ -498,6 +490,7 @@ export type ReactClientValue =
|
|||||||
| React$Element<string>
|
| React$Element<string>
|
||||||
| React$Element<ClientReference<any> & any>
|
| React$Element<ClientReference<any> & any>
|
||||||
| ReactComponentInfo
|
| ReactComponentInfo
|
||||||
|
| ReactErrorInfo
|
||||||
| string
|
| string
|
||||||
| boolean
|
| boolean
|
||||||
| number
|
| number
|
||||||
@@ -4171,6 +4164,11 @@ function serializeErrorValue(request: Request, error: Error): string {
|
|||||||
stack = [];
|
stack = [];
|
||||||
}
|
}
|
||||||
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
|
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);
|
const id = outlineModel(request, errorInfo);
|
||||||
return '$Z' + id.toString(16);
|
return '$Z' + id.toString(16);
|
||||||
} else {
|
} 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__) {
|
if (__DEV__) {
|
||||||
let name: string = 'Error';
|
let name: string = 'Error';
|
||||||
let message: string;
|
let message: string;
|
||||||
@@ -4203,6 +4205,12 @@ function serializeDebugErrorValue(request: Request, error: Error): string {
|
|||||||
stack = [];
|
stack = [];
|
||||||
}
|
}
|
||||||
const errorInfo: ReactErrorInfoDev = {name, message, stack, env};
|
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(
|
const id = outlineDebugModel(
|
||||||
request,
|
request,
|
||||||
{objectLimit: stack.length * 2 + 1},
|
{objectLimit: stack.length * 2 + 1},
|
||||||
@@ -4231,6 +4239,7 @@ function emitErrorChunk(
|
|||||||
let message: string;
|
let message: string;
|
||||||
let stack: ReactStackTrace;
|
let stack: ReactStackTrace;
|
||||||
let env = (0, request.environmentName)();
|
let env = (0, request.environmentName)();
|
||||||
|
let causeReference: null | string = null;
|
||||||
try {
|
try {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
name = error.name;
|
name = error.name;
|
||||||
@@ -4243,6 +4252,13 @@ function emitErrorChunk(
|
|||||||
// Keep the environment name.
|
// Keep the environment name.
|
||||||
env = errorEnv;
|
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) {
|
} else if (typeof error === 'object' && error !== null) {
|
||||||
message = describeObjectForErrorMessage(error);
|
message = describeObjectForErrorMessage(error);
|
||||||
stack = [];
|
stack = [];
|
||||||
@@ -4258,6 +4274,9 @@ function emitErrorChunk(
|
|||||||
const ownerRef =
|
const ownerRef =
|
||||||
owner == null ? null : outlineComponentInfo(request, owner);
|
owner == null ? null : outlineComponentInfo(request, owner);
|
||||||
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
|
errorInfo = {digest, name, message, stack, env, owner: ownerRef};
|
||||||
|
if (causeReference !== null) {
|
||||||
|
(errorInfo: ReactErrorInfoDev).cause = causeReference;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errorInfo = {digest};
|
errorInfo = {digest};
|
||||||
}
|
}
|
||||||
@@ -4969,7 +4988,7 @@ function renderDebugModel(
|
|||||||
return serializeDebugFormData(request, value);
|
return serializeDebugFormData(request, value);
|
||||||
}
|
}
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
return serializeDebugErrorValue(request, value);
|
return serializeDebugErrorValue(request, counter, value);
|
||||||
}
|
}
|
||||||
if (value instanceof ArrayBuffer) {
|
if (value instanceof ArrayBuffer) {
|
||||||
return serializeDebugTypedArray(request, 'A', new Uint8Array(value));
|
return serializeDebugTypedArray(request, 'A', new Uint8Array(value));
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ export type ReactErrorInfoProd = {
|
|||||||
+digest: string,
|
+digest: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JSONValue =
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| number
|
||||||
|
| null
|
||||||
|
| {+[key: string]: JSONValue}
|
||||||
|
| $ReadOnlyArray<JSONValue>;
|
||||||
|
|
||||||
export type ReactErrorInfoDev = {
|
export type ReactErrorInfoDev = {
|
||||||
+digest?: string,
|
+digest?: string,
|
||||||
+name: string,
|
+name: string,
|
||||||
@@ -235,6 +243,7 @@ export type ReactErrorInfoDev = {
|
|||||||
+stack: ReactStackTrace,
|
+stack: ReactStackTrace,
|
||||||
+env: string,
|
+env: string,
|
||||||
+owner?: null | string,
|
+owner?: null | string,
|
||||||
|
cause?: JSONValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
|
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
|
||||||
|
|||||||
Reference in New Issue
Block a user