@samitouri / QOS-React-2 / commits / 7ce677d406

[Fizz] Guard the shell-error callbacks instead of nulling them out on the request (#36916)

Follow-up to #36903 The previous change stopped `onAllReady` from firing after a fatal shell error by assigning `noop` to `request.onAllReady` inside `fatalError`. This mutated a completion callback on the request as a side channel, mirroring the existing `request.onShellError = noop` in `completeShell`. This refactor removes both noop assignments and instead guards the calls at the point where they would happen, so the callbacks on the request stay the ones the caller passed in. For `onAllReady`, `erroredTask` now returns right after `fatalError` in the root (`boundary === null`) case, so it no longer falls through to the `allPendingTasks === 0` check that calls `completeAll`. This matches the sibling `finishAbortedTask`, which already returns after a fatal root error. Later task completions cannot reach `completeAll` either, because `performWork` bails out once the request status is past `OPEN`. For `onShellError`, `fatalError` now decides whether to call it based on the same condition that governed the assignment: the shell is complete exactly when `request.pendingRootTasks === 0`, and `completeShell` only runs at that point, so guarding the call is equivalent to the previous nulling out. `onFatalError` still always fires because the error is always fatal to the request. This is a behavior-preserving refactor of the fix landed in #36903; the tests added there continue to pass unchanged. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

Sebastian "Sebbie" Silbermann committed Jul 2, 2026 at 11:27 UTC 7ce677d40659f0fefd8ce122480716d0c8e926b4
1 file changed +15 -8
packages/react-server/src/ReactFizzServer.js
+15 -8
@@ -1378,15 +1378,19 @@ function fatalError(
1378 // It's also called if React itself or its host configs errors.
1379 const onShellError = request.onShellError;
1380 const onFatalError = request.onFatalError;
1381 - // The shell has fatally errored, so the render can never complete. Prevent a
1382 - // later completeAll from invoking onAllReady, which would signal a successful
1383 - // render to consumers waiting on all content.
1384 - request.onAllReady = noop;
1381 + // Once the shell has completed it can't error anymore, so onShellError only
1382 + // fires while root tasks are still pending. onFatalError always fires because
1383 + // the error is always fatal to the request.
1384 + const shellComplete = request.pendingRootTasks === 0;
1385 if (__DEV__ && debugTask) {
1386 - debugTask.run(onShellError.bind(null, error));
1386 + if (!shellComplete) {
1387 + debugTask.run(onShellError.bind(null, error));
1388 + }
1389 debugTask.run(onFatalError.bind(null, error));
1390 } else {
1389 - onShellError(error);
1391 + if (!shellComplete) {
1392 + onShellError(error);
1393 + }
1394 onFatalError(error);
1395 }
1396 if (request.destination !== null) {
@@ -4509,6 +4513,11 @@ function erroredTask(
4513 const errorDigest = logRecoverableError(request, error, errorInfo, debugTask);
4514 if (boundary === null) {
4515 fatalError(request, error, errorInfo, debugTask);
4516 + // The shell fatally errored, so the render can never complete. Return before
4517 + // the completeAll check below so we don't fire onAllReady for a render that
4518 + // produced nothing. This mirrors finishAbortedTask, which also returns after
4519 + // a fatalError on the root.
4520 + return;
4521 } else {
4522 boundary.pendingTasks--;
4523 if (boundary.status !== CLIENT_RENDERED) {
@@ -4960,8 +4969,6 @@ function completeShell(request: Request) {
4969 preparePreamble(request);
4970 }
4971
4963 - // We have completed the shell so the shell can't error anymore.
4964 - request.onShellError = noop;
4972 const onShellReady = request.onShellReady;
4973 onShellReady();
4974 }