Skip to content

Commit

Permalink
Serialize Error values
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Sep 30, 2024
1 parent c67e241 commit 775defa
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 38 deletions.
74 changes: 36 additions & 38 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,21 @@ function parseModelString(
createFormData,
);
}
case 'Z': {
// Error
if (__DEV__) {
const ref = value.slice(2);
return getOutlinedModel(
response,
ref,
parentObject,
key,
resolveErrorDev,
);
} else {
return resolveErrorProd(response);
}
}
case 'i': {
// Iterator
const ref = value.slice(2);
Expand Down Expand Up @@ -1881,11 +1896,7 @@ function formatV8Stack(
}

type ErrorWithDigest = Error & {digest?: string};
function resolveErrorProd(
response: Response,
id: number,
digest: string,
): void {
function resolveErrorProd(response: Response): Error {
if (__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
Expand All @@ -1899,25 +1910,17 @@ function resolveErrorProd(
' may provide additional details about the nature of the error.',
);
error.stack = 'Error: ' + error.message;
(error: any).digest = digest;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
triggerErrorOnChunk(chunk, errorWithDigest);
}
return error;
}

function resolveErrorDev(
response: Response,
id: number,
digest: string,
message: string,
stack: ReactStackTrace,
env: string,
): void {
errorInfo: {message: string, stack: ReactStackTrace, env: string, ...},
): Error {
const message: string = errorInfo.message;
const stack: ReactStackTrace = errorInfo.stack;
const env: string = errorInfo.env;

if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
Expand Down Expand Up @@ -1957,16 +1960,8 @@ function resolveErrorDev(
}
}

(error: any).digest = digest;
(error: any).environmentName = env;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
triggerErrorOnChunk(chunk, errorWithDigest);
}
return error;
}

function resolvePostponeProd(response: Response, id: number): void {
Expand Down Expand Up @@ -2622,17 +2617,20 @@ function processFullStringRow(
}
case 69 /* "E" */: {
const errorInfo = JSON.parse(row);
let error;
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
errorInfo.env,
);
error = resolveErrorDev(response, errorInfo);
} else {
error = resolveErrorProd(response);
}
(error: any).digest = errorInfo.digest;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createErrorChunk(response, errorWithDigest));
} else {
resolveErrorProd(response, id, errorInfo.digest);
triggerErrorOnChunk(chunk, errorWithDigest);
}
return;
}
Expand Down
40 changes: 40 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,46 @@ describe('ReactFlight', () => {
`);
});

it('can transport Error objects as values', async () => {
function ComponentClient({prop}) {
return `
is error: ${prop instanceof Error}
message: ${prop.message}
stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
environmentName: ${prop.environmentName}
`;
}
const Component = clientReference(ComponentClient);

function ServerComponent() {
const error = new Error('hello');
return <Component prop={error} />;
}

const transport = ReactNoopFlightServer.render(<ServerComponent />);

await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});

if (__DEV__) {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
message: hello
stack: Error: hello
in ServerComponent (at **)
environmentName: Server
`);
} else {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
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
`);
}
});

it('can transport cyclic objects', async () => {
function ComponentClient({prop}) {
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
Expand Down
36 changes: 36 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2688,6 +2688,9 @@ function renderModelDestructive(
if (typeof FormData === 'function' && value instanceof FormData) {
return serializeFormData(request, value);
}
if (value instanceof Error) {
return serializeErrorValue(request, value);
}

if (enableBinaryFlight) {
if (value instanceof ArrayBuffer) {
Expand Down Expand Up @@ -3114,6 +3117,36 @@ function emitPostponeChunk(
request.completedErrorChunks.push(processedChunk);
}

function serializeErrorValue(request: Request, error: Error): string {
if (__DEV__) {
let message;
let stack: ReactStackTrace;
let env = (0, request.environmentName)();
try {
// eslint-disable-next-line react-internal/safe-string-coercion
message = String(error.message);
stack = filterStackTrace(request, error, 0);
const errorEnv = (error: any).environmentName;
if (typeof errorEnv === 'string') {
// This probably came from another FlightClient as a pass through.
// Keep the environment name.
env = errorEnv;
}
} catch (x) {
message = 'An error occurred but serializing the error message failed.';
stack = [];
}
const errorInfo = {message, stack, env};
const id = outlineModel(request, errorInfo);
return '$Z' + id.toString(16);
} else {
// In prod we don't emit any information about this Error object to avoid
// unintentional leaks. Since this doesn't actually throw on the server
// we don't go through onError and so don't register any digest neither.
return '$Z';
}
}

function emitErrorChunk(
request: Request,
id: number,
Expand Down Expand Up @@ -3403,6 +3436,9 @@ function renderConsoleValue(
if (typeof FormData === 'function' && value instanceof FormData) {
return serializeFormData(request, value);
}
if (value instanceof Error) {
return serializeErrorValue(request, value);
}

if (enableBinaryFlight) {
if (value instanceof ArrayBuffer) {
Expand Down

0 comments on commit 775defa

Please sign in to comment.