[Fizz] Add Owner Stacks when render is aborted (#32735)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2025-06-02 19:27:49 +02:00
committed by GitHub
parent 526dd340b3
commit 4a1f29079c
3 changed files with 81 additions and 1 deletions

View File

@@ -364,6 +364,7 @@ function render(children: React$Element<any>, options?: Options): Destination {
children,
null,
null,
null,
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onAllReady : undefined,

View File

@@ -4762,6 +4762,27 @@ function abortTask(task: Task, request: Request, error: mixed): void {
}
}
function abortTaskDEV(task: Task, request: Request, error: mixed): void {
if (__DEV__) {
const prevTaskInDEV = currentTaskInDEV;
const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
setCurrentTaskInDEV(task);
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
try {
abortTask(task, request, error);
} finally {
setCurrentTaskInDEV(prevTaskInDEV);
ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
}
} else {
// 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
throw new Error(
'abortTaskDEV should never be called in production mode. This is a bug in React.',
);
}
}
function safelyEmitEarlyPreloads(
request: Request,
shellComplete: boolean,
@@ -6111,7 +6132,11 @@ export function abort(request: Request, reason: mixed): void {
// This error isn't necessarily fatal in this case but we need to stash it
// so we can use it to abort any pending work
request.fatalError = error;
abortableTasks.forEach(task => abortTask(task, request, error));
if (__DEV__) {
abortableTasks.forEach(task => abortTaskDEV(task, request, error));
} else {
abortableTasks.forEach(task => abortTask(task, request, error));
}
abortableTasks.clear();
}
if (request.destination !== null) {

View File

@@ -10,13 +10,28 @@
'use strict';
let act;
let React;
let ReactNoopServer;
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
const dot = name.lastIndexOf('.');
if (dot !== -1) {
name = name.slice(dot + 1);
}
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
describe('ReactServer', () => {
beforeEach(() => {
jest.resetModules();
act = require('internal-test-utils').act;
React = require('react');
ReactNoopServer = require('react-noop-renderer/server');
});
@@ -32,4 +47,43 @@ describe('ReactServer', () => {
const result = ReactNoopServer.render(<div>hello world</div>);
expect(result.root).toEqual(div('hello world'));
});
it('has Owner Stacks in DEV when aborted', async () => {
function Component({promise}) {
React.use(promise);
return <div>Hello, Dave!</div>;
}
function App({promise}) {
return <Component promise={promise} />;
}
let caughtError;
let componentStack;
let ownerStack;
const result = ReactNoopServer.render(
<App promise={new Promise(() => {})} />,
{
onError: (error, errorInfo) => {
caughtError = error;
componentStack = errorInfo.componentStack;
ownerStack = __DEV__ ? React.captureOwnerStack() : null;
},
},
);
await act(async () => {
result.abort();
});
expect(caughtError).toEqual(
expect.objectContaining({
message: 'The render was aborted by the server without a reason.',
}),
);
expect(normalizeCodeLocInfo(componentStack)).toEqual(
'\n in Component (at **)' + '\n in App (at **)',
);
expect(normalizeCodeLocInfo(ownerStack)).toEqual(
__DEV__ ? '\n in App (at **)' : null,
);
});
});