Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Serialize Error Values #31104

Merged
merged 1 commit into from
Sep 30, 2024
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
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
Loading