Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {
JSONValue,
Thenable,
ReactDebugInfo,
ReactDebugInfoEntry,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -3537,6 +3542,7 @@ function resolveErrorDev(
null,
message ||
'An error occurred in the Server Components render but no message was provided',
errorOptions,
),
);

Expand Down
133 changes: 133 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 11 additions & 1 deletion packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -7041,7 +7041,17 @@ export function hoistHoistables(
}
}

export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
export function hasSuspenseyContent(
hoistableState: HoistableState,
flushingInShell: boolean,
): boolean {
if (flushingInShell) {
// When flushing the shell, stylesheets with precedence are already emitted
// in the <head> which blocks paint. There's no benefit to outlining for CSS
// alone during the shell flush. However, suspensey images (for ViewTransition
// animation reveals) should still trigger outlining even during the shell.
return hoistableState.suspenseyImages;
}
return hoistableState.stylesheets.size > 0 || hoistableState.suspenseyImages;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,10 @@ export function writePreambleStart(
);
}

export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
export function hasSuspenseyContent(
hoistableState: HoistableState,
flushingInShell: boolean,
): boolean {
// Never outline.
return false;
}
Expand Down
Loading
Loading