Skip to content

Commit 3cf7160

Browse files
committed
[Flight] Transport AggregateErrors.errors
1 parent e7cba82 commit 3cf7160

File tree

4 files changed

+259
-7
lines changed

4 files changed

+259
-7
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3537,18 +3537,40 @@ function resolveErrorDev(
35373537
),
35383538
}
35393539
: undefined;
3540+
const isAggregateError =
3541+
typeof AggregateError !== 'undefined' && 'errors' in errorInfo;
3542+
const revivedErrors =
3543+
// We don't serialize AggregateError.errors in prod so we never need to deserialize
3544+
__DEV__ && isAggregateError
3545+
? reviveModel(
3546+
response,
3547+
// $FlowFixMe[incompatible-cast]
3548+
(errorInfo.errors: JSONValue),
3549+
errorInfo,
3550+
'errors',
3551+
)
3552+
: null;
35403553
const callStack = buildFakeCallStack(
35413554
response,
35423555
stack,
35433556
env,
35443557
false,
3545-
// $FlowFixMe[incompatible-use]
3546-
Error.bind(
3547-
null,
3548-
message ||
3549-
'An error occurred in the Server Components render but no message was provided',
3550-
errorOptions,
3551-
),
3558+
isAggregateError
3559+
? // $FlowFixMe[incompatible-use]
3560+
AggregateError.bind(
3561+
null,
3562+
revivedErrors,
3563+
message ||
3564+
'An error occurred in the Server Components render but no message was provided',
3565+
errorOptions,
3566+
)
3567+
: // $FlowFixMe[incompatible-use]
3568+
Error.bind(
3569+
null,
3570+
message ||
3571+
'An error occurred in the Server Components render but no message was provided',
3572+
errorOptions,
3573+
),
35523574
);
35533575

35543576
let ownerTask: null | ConsoleTask = null;

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,204 @@ describe('ReactFlight', () => {
840840
}
841841
});
842842

843+
it('can transport AggregateError', async () => {
844+
function renderError(error) {
845+
if (!(error instanceof Error)) {
846+
return `${JSON.stringify(error)}`;
847+
}
848+
let result = `
849+
is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
850+
name: ${error.name}
851+
message: ${error.message}
852+
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
853+
environmentName: ${error.environmentName}
854+
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
855+
if ('errors' in error) {
856+
result += `
857+
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
858+
}
859+
return result;
860+
}
861+
function ComponentClient({error}) {
862+
return renderError(error);
863+
}
864+
const Component = clientReference(ComponentClient);
865+
866+
function ServerComponent() {
867+
const error1 = new TypeError('first error');
868+
const error2 = new RangeError('second error');
869+
const error = new AggregateError([error1, error2], 'aggregate');
870+
return <Component error={error} />;
871+
}
872+
873+
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
874+
onError(x) {
875+
if (__DEV__) {
876+
return 'a dev digest';
877+
}
878+
return `digest("${x.message}")`;
879+
},
880+
});
881+
882+
await act(() => {
883+
ReactNoop.render(ReactNoopFlightClient.read(transport));
884+
});
885+
886+
if (__DEV__) {
887+
expect(ReactNoop).toMatchRenderedOutput(`
888+
is error: AggregateError
889+
name: AggregateError
890+
message: aggregate
891+
stack: AggregateError: aggregate
892+
in ServerComponent (at **)
893+
environmentName: Server
894+
cause: no cause
895+
errors: [
896+
is error: Error
897+
name: TypeError
898+
message: first error
899+
stack: TypeError: first error
900+
in ServerComponent (at **)
901+
environmentName: Server
902+
cause: no cause,
903+
904+
is error: Error
905+
name: RangeError
906+
message: second error
907+
stack: RangeError: second error
908+
in ServerComponent (at **)
909+
environmentName: Server
910+
cause: no cause]`);
911+
} else {
912+
expect(ReactNoop).toMatchRenderedOutput(`
913+
is error: Error
914+
name: Error
915+
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.
916+
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.
917+
environmentName: undefined
918+
cause: no cause`);
919+
}
920+
});
921+
922+
it('includes AggregateError.errors in thrown errors', async () => {
923+
function renderError(error) {
924+
if (!(error instanceof Error)) {
925+
return `${JSON.stringify(error)}`;
926+
}
927+
let result = `
928+
is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
929+
name: ${error.name}
930+
message: ${error.message}
931+
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
932+
environmentName: ${error.environmentName}
933+
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
934+
if ('errors' in error) {
935+
result += `
936+
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
937+
}
938+
return result;
939+
}
940+
941+
function ServerComponent() {
942+
const error1 = new TypeError('first error');
943+
const error2 = new RangeError('second error');
944+
const error3 = new Error('third error');
945+
const error4 = new Error('fourth error');
946+
const error5 = new Error('fifth error');
947+
const error6 = new Error('sixth error');
948+
const error = new AggregateError(
949+
[error1, error2, error3, error4, error5, error6],
950+
'aggregate',
951+
);
952+
throw error;
953+
}
954+
955+
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
956+
onError(x) {
957+
if (__DEV__) {
958+
return 'a dev digest';
959+
}
960+
return `digest("${x.message}")`;
961+
},
962+
});
963+
964+
let error;
965+
try {
966+
await act(() => {
967+
ReactNoop.render(ReactNoopFlightClient.read(transport));
968+
});
969+
} catch (x) {
970+
error = x;
971+
}
972+
973+
if (__DEV__) {
974+
expect(renderError(error)).toEqual(`
975+
is error: AggregateError
976+
name: AggregateError
977+
message: aggregate
978+
stack: AggregateError: aggregate
979+
in ServerComponent (at **)
980+
environmentName: Server
981+
cause: no cause
982+
errors: [
983+
is error: Error
984+
name: TypeError
985+
message: first error
986+
stack: TypeError: first error
987+
in ServerComponent (at **)
988+
environmentName: Server
989+
cause: no cause,
990+
991+
is error: Error
992+
name: RangeError
993+
message: second error
994+
stack: RangeError: second error
995+
in ServerComponent (at **)
996+
environmentName: Server
997+
cause: no cause,
998+
999+
is error: Error
1000+
name: Error
1001+
message: third error
1002+
stack: Error: third error
1003+
in ServerComponent (at **)
1004+
environmentName: Server
1005+
cause: no cause,
1006+
1007+
is error: Error
1008+
name: Error
1009+
message: fourth error
1010+
stack: Error: fourth error
1011+
in ServerComponent (at **)
1012+
environmentName: Server
1013+
cause: no cause,
1014+
1015+
is error: Error
1016+
name: Error
1017+
message: fifth error
1018+
stack: Error: fifth error
1019+
in ServerComponent (at **)
1020+
environmentName: Server
1021+
cause: no cause,
1022+
1023+
is error: Error
1024+
name: Error
1025+
message: sixth error
1026+
stack: Error: sixth error
1027+
in ServerComponent (at **)
1028+
environmentName: Server
1029+
cause: no cause]`);
1030+
} else {
1031+
expect(renderError(error)).toEqual(`
1032+
is error: Error
1033+
name: Error
1034+
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.
1035+
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.
1036+
environmentName: undefined
1037+
cause: no cause`);
1038+
}
1039+
});
1040+
8431041
it('can transport cyclic objects', async () => {
8441042
function ComponentClient({prop}) {
8451043
expect(prop.obj.obj.obj).toBe(prop.obj.obj);

packages/react-server/src/ReactFlightServer.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4169,6 +4169,14 @@ function serializeErrorValue(request: Request, error: Error): string {
41694169
const causeId = outlineModel(request, cause);
41704170
errorInfo.cause = serializeByValueID(causeId);
41714171
}
4172+
if (
4173+
typeof AggregateError !== 'undefined' &&
4174+
error instanceof AggregateError
4175+
) {
4176+
const errors: ReactClientValue = (error.errors: any);
4177+
const errorsId = outlineModel(request, errors);
4178+
errorInfo.errors = serializeByValueID(errorsId);
4179+
}
41724180
const id = outlineModel(request, errorInfo);
41734181
return '$Z' + id.toString(16);
41744182
} else {
@@ -4211,6 +4219,15 @@ function serializeDebugErrorValue(
42114219
const causeId = outlineDebugModel(request, counter, cause);
42124220
errorInfo.cause = serializeByValueID(causeId);
42134221
}
4222+
if (
4223+
typeof AggregateError !== 'undefined' &&
4224+
error instanceof AggregateError
4225+
) {
4226+
counter.objectLimit--;
4227+
const errors: ReactClientValue = (error.errors: any);
4228+
const errorsId = outlineDebugModel(request, counter, errors);
4229+
errorInfo.errors = serializeByValueID(errorsId);
4230+
}
42144231
const id = outlineDebugModel(
42154232
request,
42164233
{objectLimit: stack.length * 2 + 1},
@@ -4240,6 +4257,7 @@ function emitErrorChunk(
42404257
let stack: ReactStackTrace;
42414258
let env = (0, request.environmentName)();
42424259
let causeReference: null | string = null;
4260+
let errorsReference: null | string = null;
42434261
try {
42444262
if (error instanceof Error) {
42454263
name = error.name;
@@ -4259,6 +4277,16 @@ function emitErrorChunk(
42594277
: outlineModel(request, cause);
42604278
causeReference = serializeByValueID(causeId);
42614279
}
4280+
if (
4281+
typeof AggregateError !== 'undefined' &&
4282+
error instanceof AggregateError
4283+
) {
4284+
const errors: ReactClientValue = (error.errors: any);
4285+
const errorsId = debug
4286+
? outlineDebugModel(request, {objectLimit: 5}, errors)
4287+
: outlineModel(request, errors);
4288+
errorsReference = serializeByValueID(errorsId);
4289+
}
42624290
} else if (typeof error === 'object' && error !== null) {
42634291
message = describeObjectForErrorMessage(error);
42644292
stack = [];
@@ -4277,6 +4305,9 @@ function emitErrorChunk(
42774305
if (causeReference !== null) {
42784306
(errorInfo: ReactErrorInfoDev).cause = causeReference;
42794307
}
4308+
if (errorsReference !== null) {
4309+
(errorInfo: ReactErrorInfoDev).errors = errorsReference;
4310+
}
42804311
} else {
42814312
errorInfo = {digest};
42824313
}

packages/shared/ReactTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ export type ReactErrorInfoDev = {
244244
+env: string,
245245
+owner?: null | string,
246246
cause?: JSONValue,
247+
errors?: JSONValue,
247248
};
248249

249250
export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;

0 commit comments

Comments
 (0)