Skip to content

Commit 775defa

Browse files
committed
Serialize Error values
1 parent c67e241 commit 775defa

File tree

3 files changed

+112
-38
lines changed

3 files changed

+112
-38
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,21 @@ function parseModelString(
12871287
createFormData,
12881288
);
12891289
}
1290+
case 'Z': {
1291+
// Error
1292+
if (__DEV__) {
1293+
const ref = value.slice(2);
1294+
return getOutlinedModel(
1295+
response,
1296+
ref,
1297+
parentObject,
1298+
key,
1299+
resolveErrorDev,
1300+
);
1301+
} else {
1302+
return resolveErrorProd(response);
1303+
}
1304+
}
12901305
case 'i': {
12911306
// Iterator
12921307
const ref = value.slice(2);
@@ -1881,11 +1896,7 @@ function formatV8Stack(
18811896
}
18821897

18831898
type ErrorWithDigest = Error & {digest?: string};
1884-
function resolveErrorProd(
1885-
response: Response,
1886-
id: number,
1887-
digest: string,
1888-
): void {
1899+
function resolveErrorProd(response: Response): Error {
18891900
if (__DEV__) {
18901901
// These errors should never make it into a build so we don't need to encode them in codes.json
18911902
// eslint-disable-next-line react-internal/prod-error-codes
@@ -1899,25 +1910,17 @@ function resolveErrorProd(
18991910
' may provide additional details about the nature of the error.',
19001911
);
19011912
error.stack = 'Error: ' + error.message;
1902-
(error: any).digest = digest;
1903-
const errorWithDigest: ErrorWithDigest = (error: any);
1904-
const chunks = response._chunks;
1905-
const chunk = chunks.get(id);
1906-
if (!chunk) {
1907-
chunks.set(id, createErrorChunk(response, errorWithDigest));
1908-
} else {
1909-
triggerErrorOnChunk(chunk, errorWithDigest);
1910-
}
1913+
return error;
19111914
}
19121915

19131916
function resolveErrorDev(
19141917
response: Response,
1915-
id: number,
1916-
digest: string,
1917-
message: string,
1918-
stack: ReactStackTrace,
1919-
env: string,
1920-
): void {
1918+
errorInfo: {message: string, stack: ReactStackTrace, env: string, ...},
1919+
): Error {
1920+
const message: string = errorInfo.message;
1921+
const stack: ReactStackTrace = errorInfo.stack;
1922+
const env: string = errorInfo.env;
1923+
19211924
if (!__DEV__) {
19221925
// These errors should never make it into a build so we don't need to encode them in codes.json
19231926
// eslint-disable-next-line react-internal/prod-error-codes
@@ -1957,16 +1960,8 @@ function resolveErrorDev(
19571960
}
19581961
}
19591962

1960-
(error: any).digest = digest;
19611963
(error: any).environmentName = env;
1962-
const errorWithDigest: ErrorWithDigest = (error: any);
1963-
const chunks = response._chunks;
1964-
const chunk = chunks.get(id);
1965-
if (!chunk) {
1966-
chunks.set(id, createErrorChunk(response, errorWithDigest));
1967-
} else {
1968-
triggerErrorOnChunk(chunk, errorWithDigest);
1969-
}
1964+
return error;
19701965
}
19711966

19721967
function resolvePostponeProd(response: Response, id: number): void {
@@ -2622,17 +2617,20 @@ function processFullStringRow(
26222617
}
26232618
case 69 /* "E" */: {
26242619
const errorInfo = JSON.parse(row);
2620+
let error;
26252621
if (__DEV__) {
2626-
resolveErrorDev(
2627-
response,
2628-
id,
2629-
errorInfo.digest,
2630-
errorInfo.message,
2631-
errorInfo.stack,
2632-
errorInfo.env,
2633-
);
2622+
error = resolveErrorDev(response, errorInfo);
2623+
} else {
2624+
error = resolveErrorProd(response);
2625+
}
2626+
(error: any).digest = errorInfo.digest;
2627+
const errorWithDigest: ErrorWithDigest = (error: any);
2628+
const chunks = response._chunks;
2629+
const chunk = chunks.get(id);
2630+
if (!chunk) {
2631+
chunks.set(id, createErrorChunk(response, errorWithDigest));
26342632
} else {
2635-
resolveErrorProd(response, id, errorInfo.digest);
2633+
triggerErrorOnChunk(chunk, errorWithDigest);
26362634
}
26372635
return;
26382636
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,46 @@ describe('ReactFlight', () => {
653653
`);
654654
});
655655

656+
it('can transport Error objects as values', async () => {
657+
function ComponentClient({prop}) {
658+
return `
659+
is error: ${prop instanceof Error}
660+
message: ${prop.message}
661+
stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
662+
environmentName: ${prop.environmentName}
663+
`;
664+
}
665+
const Component = clientReference(ComponentClient);
666+
667+
function ServerComponent() {
668+
const error = new Error('hello');
669+
return <Component prop={error} />;
670+
}
671+
672+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
673+
674+
await act(async () => {
675+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
676+
});
677+
678+
if (__DEV__) {
679+
expect(ReactNoop).toMatchRenderedOutput(`
680+
is error: true
681+
message: hello
682+
stack: Error: hello
683+
in ServerComponent (at **)
684+
environmentName: Server
685+
`);
686+
} else {
687+
expect(ReactNoop).toMatchRenderedOutput(`
688+
is error: true
689+
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.
690+
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.
691+
environmentName: undefined
692+
`);
693+
}
694+
});
695+
656696
it('can transport cyclic objects', async () => {
657697
function ComponentClient({prop}) {
658698
expect(prop.obj.obj.obj).toBe(prop.obj.obj);

packages/react-server/src/ReactFlightServer.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,6 +2688,9 @@ function renderModelDestructive(
26882688
if (typeof FormData === 'function' && value instanceof FormData) {
26892689
return serializeFormData(request, value);
26902690
}
2691+
if (value instanceof Error) {
2692+
return serializeErrorValue(request, value);
2693+
}
26912694

26922695
if (enableBinaryFlight) {
26932696
if (value instanceof ArrayBuffer) {
@@ -3114,6 +3117,36 @@ function emitPostponeChunk(
31143117
request.completedErrorChunks.push(processedChunk);
31153118
}
31163119

3120+
function serializeErrorValue(request: Request, error: Error): string {
3121+
if (__DEV__) {
3122+
let message;
3123+
let stack: ReactStackTrace;
3124+
let env = (0, request.environmentName)();
3125+
try {
3126+
// eslint-disable-next-line react-internal/safe-string-coercion
3127+
message = String(error.message);
3128+
stack = filterStackTrace(request, error, 0);
3129+
const errorEnv = (error: any).environmentName;
3130+
if (typeof errorEnv === 'string') {
3131+
// This probably came from another FlightClient as a pass through.
3132+
// Keep the environment name.
3133+
env = errorEnv;
3134+
}
3135+
} catch (x) {
3136+
message = 'An error occurred but serializing the error message failed.';
3137+
stack = [];
3138+
}
3139+
const errorInfo = {message, stack, env};
3140+
const id = outlineModel(request, errorInfo);
3141+
return '$Z' + id.toString(16);
3142+
} else {
3143+
// In prod we don't emit any information about this Error object to avoid
3144+
// unintentional leaks. Since this doesn't actually throw on the server
3145+
// we don't go through onError and so don't register any digest neither.
3146+
return '$Z';
3147+
}
3148+
}
3149+
31173150
function emitErrorChunk(
31183151
request: Request,
31193152
id: number,
@@ -3403,6 +3436,9 @@ function renderConsoleValue(
34033436
if (typeof FormData === 'function' && value instanceof FormData) {
34043437
return serializeFormData(request, value);
34053438
}
3439+
if (value instanceof Error) {
3440+
return serializeErrorValue(request, value);
3441+
}
34063442

34073443
if (enableBinaryFlight) {
34083444
if (value instanceof ArrayBuffer) {

0 commit comments

Comments
 (0)