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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ module.exports = {
CallSite: 'readonly',
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AggregateError: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
Expand Down
1 change: 0 additions & 1 deletion packages/internal-test-utils/ReactInternalTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ ${diff(expectedLog, actualLog)}

function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
Expand Down
1 change: 0 additions & 1 deletion packages/internal-test-utils/internalAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ async function waitForMicrotasks() {

function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
Expand Down
39 changes: 31 additions & 8 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3525,7 +3525,8 @@ function resolveErrorDev(

let error;
const errorOptions =
'cause' in errorInfo
// We don't serialize Error.cause in prod so we never need to deserialize
__DEV__ && 'cause' in errorInfo
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to #35810. Doing the same for AggregateError.errors. Since we never serialize them on the Server in prod, we don't need to consider deserializing them in prod on the Client. Also acts as defense-in-depth.

? {
cause: reviveModel(
response,
Expand All @@ -3536,18 +3537,40 @@ function resolveErrorDev(
),
}
: undefined;
const isAggregateError =
typeof AggregateError !== 'undefined' && 'errors' in errorInfo;
const revivedErrors =
// We don't serialize AggregateError.errors in prod so we never need to deserialize
__DEV__ && isAggregateError
? reviveModel(
response,
// $FlowFixMe[incompatible-cast]
(errorInfo.errors: JSONValue),
errorInfo,
'errors',
)
: null;
const callStack = buildFakeCallStack(
response,
stack,
env,
false,
// $FlowFixMe[incompatible-use]
Error.bind(
null,
message ||
'An error occurred in the Server Components render but no message was provided',
errorOptions,
),
isAggregateError
? // $FlowFixMe[incompatible-use]
AggregateError.bind(
null,
revivedErrors,
message ||
'An error occurred in the Server Components render but no message was provided',
errorOptions,
)
: // $FlowFixMe[incompatible-use]
Error.bind(
null,
message ||
'An error occurred in the Server Components render but no message was provided',
errorOptions,
),
);

let ownerTask: null | ConsoleTask = null;
Expand Down
198 changes: 198 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,204 @@ describe('ReactFlight', () => {
}
});

it('can transport AggregateError', async () => {
function renderError(error) {
if (!(error instanceof Error)) {
return `${JSON.stringify(error)}`;
}
let result = `
is error: ${error instanceof AggregateError ? 'AggregateError' : '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'}`;
if ('errors' in error) {
result += `
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
}
return result;
}
function ComponentClient({error}) {
return renderError(error);
}
const Component = clientReference(ComponentClient);

function ServerComponent() {
const error1 = new TypeError('first error');
const error2 = new RangeError('second error');
const error = new AggregateError([error1, error2], 'aggregate');
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: AggregateError
name: AggregateError
message: aggregate
stack: AggregateError: aggregate
in ServerComponent (at **)
environmentName: Server
cause: no cause
errors: [
is error: Error
name: TypeError
message: first error
stack: TypeError: first error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: RangeError
message: second error
stack: RangeError: second error
in ServerComponent (at **)
environmentName: Server
cause: no cause]`);
} else {
expect(ReactNoop).toMatchRenderedOutput(`
is error: Error
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 AggregateError.errors in thrown errors', async () => {
function renderError(error) {
if (!(error instanceof Error)) {
return `${JSON.stringify(error)}`;
}
let result = `
is error: ${error instanceof AggregateError ? 'AggregateError' : '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'}`;
if ('errors' in error) {
result += `
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
}
return result;
}

function ServerComponent() {
const error1 = new TypeError('first error');
const error2 = new RangeError('second error');
const error3 = new Error('third error');
const error4 = new Error('fourth error');
const error5 = new Error('fifth error');
const error6 = new Error('sixth error');
const error = new AggregateError(
[error1, error2, error3, error4, error5, error6],
'aggregate',
);
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: AggregateError
name: AggregateError
message: aggregate
stack: AggregateError: aggregate
in ServerComponent (at **)
environmentName: Server
cause: no cause
errors: [
is error: Error
name: TypeError
message: first error
stack: TypeError: first error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: RangeError
message: second error
stack: RangeError: second error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: Error
message: third error
stack: Error: third error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: Error
message: fourth error
stack: Error: fourth error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: Error
message: fifth error
stack: Error: fifth error
in ServerComponent (at **)
environmentName: Server
cause: no cause,

is error: Error
name: Error
message: sixth error
stack: Error: sixth error
in ServerComponent (at **)
environmentName: Server
cause: no cause]`);
} else {
expect(renderError(error)).toEqual(`
is error: Error
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
1 change: 0 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMSelect-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,6 @@ describe('ReactDOMSelect', () => {
);
}),
).rejects.toThrowError(
// eslint-disable-next-line no-undef
new AggregateError([
new TypeError('prod message'),
new TypeError('prod message'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,6 @@ describe('ReactFlushSync', () => {
expect(getVisibleChildren(container3)).toEqual('aww');

// Because there were multiple errors, React threw an AggregateError.
// eslint-disable-next-line no-undef
expect(error).toBeInstanceOf(AggregateError);
expect(error.errors.length).toBe(2);
expect(error.errors[0]).toBe(aahh);
Expand Down
31 changes: 31 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4169,6 +4169,14 @@ function serializeErrorValue(request: Request, error: Error): string {
const causeId = outlineModel(request, cause);
errorInfo.cause = serializeByValueID(causeId);
}
if (
typeof AggregateError !== 'undefined' &&
error instanceof AggregateError
) {
const errors: ReactClientValue = (error.errors: any);
const errorsId = outlineModel(request, errors);
errorInfo.errors = serializeByValueID(errorsId);
}
const id = outlineModel(request, errorInfo);
return '$Z' + id.toString(16);
} else {
Expand Down Expand Up @@ -4211,6 +4219,15 @@ function serializeDebugErrorValue(
const causeId = outlineDebugModel(request, counter, cause);
errorInfo.cause = serializeByValueID(causeId);
}
if (
typeof AggregateError !== 'undefined' &&
error instanceof AggregateError
) {
counter.objectLimit--;
const errors: ReactClientValue = (error.errors: any);
const errorsId = outlineDebugModel(request, counter, errors);
errorInfo.errors = serializeByValueID(errorsId);
}
const id = outlineDebugModel(
request,
{objectLimit: stack.length * 2 + 1},
Expand Down Expand Up @@ -4240,6 +4257,7 @@ function emitErrorChunk(
let stack: ReactStackTrace;
let env = (0, request.environmentName)();
let causeReference: null | string = null;
let errorsReference: null | string = null;
try {
if (error instanceof Error) {
name = error.name;
Expand All @@ -4259,6 +4277,16 @@ function emitErrorChunk(
: outlineModel(request, cause);
causeReference = serializeByValueID(causeId);
}
if (
typeof AggregateError !== 'undefined' &&
error instanceof AggregateError
) {
const errors: ReactClientValue = (error.errors: any);
const errorsId = debug
? outlineDebugModel(request, {objectLimit: 5}, errors)
: outlineModel(request, errors);
errorsReference = serializeByValueID(errorsId);
}
} else if (typeof error === 'object' && error !== null) {
message = describeObjectForErrorMessage(error);
stack = [];
Expand All @@ -4277,6 +4305,9 @@ function emitErrorChunk(
if (causeReference !== null) {
(errorInfo: ReactErrorInfoDev).cause = causeReference;
}
if (errorsReference !== null) {
(errorInfo: ReactErrorInfoDev).errors = errorsReference;
}
} else {
errorInfo = {digest};
}
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/ReactAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ let didWarnNoAwaitAct = false;

function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
Expand Down
1 change: 1 addition & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export type ReactErrorInfoDev = {
+env: string,
+owner?: null | string,
cause?: JSONValue,
errors?: JSONValue,
};

export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
Expand Down
Loading