Skip to content

Commit

Permalink
support errorInfo in onRecoverableError (#24591)
Browse files Browse the repository at this point in the history
* extend onRecoverableError API to support errorInfo

errorInfo has been used in Error Boundaries wiht componentDidCatch for a while now. To date this metadata only contained a componentStack. onRecoverableError only receives an error (type mixed) argument and thus providing additional error metadata was not possible without mutating user created mixed objects.

This change modifies rootConcurrentErrors rootRecoverableErrors, and hydrationErrors so all expect CapturedValue types. additionally a new factory function allows the creation of CapturedValues from a value plus a hash and stack.

In general, client derived CapturedValues will be created using the original function which derives a componentStack from a fiber and server originated CapturedValues will be created using with a passed in hash and optional componentStack.
  • Loading branch information
gnoff committed Jun 6, 2022
1 parent 254b49e commit 4f29ba1
Show file tree
Hide file tree
Showing 22 changed files with 495 additions and 267 deletions.
191 changes: 119 additions & 72 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -90,46 +90,28 @@ describe('ReactDOMFizzServer', () => {
});

function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
const mappedErrows = errorsArr.map(error => {
if (error.componentStack) {
return [
error.message,
error.hash,
normalizeCodeLocInfo(error.componentStack),
];
} else if (error.hash) {
return [error.message, error.hash];
const mappedErrows = errorsArr.map(({error, errorInfo}) => {
const stack = errorInfo && errorInfo.componentStack;
const digest = errorInfo && errorInfo.digest;
if (stack) {
return [error.message, digest, normalizeCodeLocInfo(stack)];
} else if (digest) {
return [error.message, digest];
}
return error.message;
});
if (__DEV__) {
expect(mappedErrows).toEqual(
toBeDevArr,
// .map(([errorMessage, errorHash, errorComponentStack]) => {
// if (typeof error === 'string' || error instanceof String) {
// return error;
// }
// let str = JSON.stringify(error).replace(/\\n/g, '\n');
// // this gets stripped away by normalizeCodeLocInfo...
// // Kind of hacky but lets strip it away here too just so they match...
// // easier than fixing the regex to account for this edge case
// if (str.endsWith('at **)"}')) {
// str = str.replace(/at \*\*\)\"}$/, 'at **)');
// }
// return str;
// }),
);
expect(mappedErrows).toEqual(toBeDevArr);
} else {
expect(mappedErrows).toEqual(toBeProdArr);
}
}

// @TODO we will use this in a followup change once we start exposing componentStacks from server errors
// function componentStack(components) {
// return components
// .map(component => `\n in ${component} (at **)`)
// .join('');
// }
function componentStack(components) {
return components
.map(component => `\n in ${component} (at **)`)
.join('');
}

async function act(callback) {
await callback();
Expand Down Expand Up @@ -471,8 +453,8 @@ describe('ReactDOMFizzServer', () => {
bootstrapped = true;
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
};
Expand All @@ -483,8 +465,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'Hash of (' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand Down Expand Up @@ -519,9 +501,18 @@ describe('ReactDOMFizzServer', () => {
expect(Scheduler).toFlushAndYield([]);
expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack(['Lazy', 'Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);

Expand Down Expand Up @@ -577,8 +568,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash of (' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

function App({isClient}) {
return (
Expand All @@ -605,8 +596,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -630,9 +621,18 @@ describe('ReactDOMFizzServer', () => {

expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack(['Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);

Expand Down Expand Up @@ -675,8 +675,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash(' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -693,8 +693,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -703,9 +703,18 @@ describe('ReactDOMFizzServer', () => {

expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack(['Erroring', 'Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);
});
Expand Down Expand Up @@ -735,8 +744,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return 'hash(' + x.message + ')';
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -753,8 +762,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -773,9 +782,18 @@ describe('ReactDOMFizzServer', () => {

expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack(['Lazy', 'Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);

Expand Down Expand Up @@ -1053,9 +1071,10 @@ describe('ReactDOMFizzServer', () => {
}

const loggedErrors = [];
const expectedDigest = 'Hash for Abort';
function onError(error) {
loggedErrors.push(error);
return `Hash of (${error.message})`;
return expectedDigest;
}

let controls;
Expand All @@ -1069,8 +1088,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand All @@ -1087,9 +1106,12 @@ describe('ReactDOMFizzServer', () => {
expect(Scheduler).toFlushAndYield([]);
expectErrors(
errors,
['This Suspense boundary was aborted by the server'],
[['This Suspense boundary was aborted by the server.', expectedDigest]],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
Expand Down Expand Up @@ -1755,8 +1777,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return `hash of (${x.message})`;
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

let controls;
await act(async () => {
Expand All @@ -1775,8 +1797,8 @@ describe('ReactDOMFizzServer', () => {
const errors = [];
// Attempt to hydrate the content.
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
Scheduler.unstable_flushAll();
Expand Down Expand Up @@ -1809,9 +1831,25 @@ describe('ReactDOMFizzServer', () => {
expect(Scheduler).toFlushAndYield([]);
expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack([
'AsyncText',
'h1',
'Suspense',
'div',
'Suspense',
'App',
]),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);

Expand Down Expand Up @@ -3142,8 +3180,8 @@ describe('ReactDOMFizzServer', () => {
loggedErrors.push(x);
return x.message.replace('bad message', 'bad hash');
}
// const expectedHash = onError(theError);
// loggedErrors.length = 0;
const expectedDigest = onError(theError);
loggedErrors.length = 0;

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
Expand All @@ -3156,18 +3194,27 @@ describe('ReactDOMFizzServer', () => {

const errors = [];
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error);
onRecoverableError(error, errorInfo) {
errors.push({error, errorInfo});
},
});
expect(Scheduler).toFlushAndYield([]);

// If escaping were not done we would get a message that says "bad hash"
expectErrors(
errors,
[theError.message],
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
[
theError.message,
expectedDigest,
componentStack(['Erroring', 'Suspense', 'div', 'App']),
],
],
[
[
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
expectedDigest,
],
],
);
});
Expand Down
Expand Up @@ -215,7 +215,7 @@ describe('ReactDOMFizzServer', () => {
expect(result).toContain('Loading');

expect(errors).toEqual([
'This Suspense boundary was aborted by the server',
'This Suspense boundary was aborted by the server.',
]);
});

Expand Down Expand Up @@ -256,7 +256,7 @@ describe('ReactDOMFizzServer', () => {
reader.cancel();

expect(errors).toEqual([
'This Suspense boundary was aborted by the server',
'This Suspense boundary was aborted by the server.',
]);

hasLoaded = true;
Expand Down

0 comments on commit 4f29ba1

Please sign in to comment.