@samitouri / QOS-React-1 / commits / 5e9eedb578

[Flight] Clear chunk reason after successful module initialization (#36024)

When `requireModule` triggers a reentrant `readChunk` on the same module chunk, the reentrant call can fail and set `chunk.reason` to an error. After the outer `requireModule` succeeds, the chunk transitions to initialized but retains the stale error as `reason`. When the Flight response stream later closes, it iterates all chunks and expects `reason` on initialized chunks to be a `FlightStreamController`. Since the stale `reason` is an `Error` object instead, calling `chunk.reason.error()` crashes with `TypeError: chunk.reason.error is not a function`. The reentrancy can occur when module evaluation synchronously triggers `readChunk` on the same chunk — for example, when code called during evaluation tries to resolve the client reference for the module that is currently being initialized. In Fizz SSR, `captureOwnerStack()` can trigger this because it constructs component stacks that resolve lazy client references via `readChunk`. The reentrant `requireModule` call returns the module's namespace object, but since the module is still being evaluated, accessing the export binding throws a TDZ (Temporal Dead Zone) `ReferenceError`. This sets the chunk to the errored state, and the `ReferenceError` becomes the stale `chunk.reason` after the outer call succeeds. This scenario is triggered in Next.js when a client module calls an instrumented API like `Math.random()` in module scope, which synchronously invokes `captureOwnerStack()`.

Hendrik Liebau committed Mar 12, 2026 at 19:17 UTC 5e9eedb57843dc161defdf93ed457ab7d982e324
3 files changed +94
packages/react-client/src/ReactFlightClient.js
+4
@@ -1040,6 +1040,8 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
1040 // Initialize any debug info and block the initializing chunk on any
1041 // unresolved entries.
1042 initializeDebugChunk(response, chunk);
1043 + // TODO: The chunk might have transitioned to ERRORED now.
1044 + // Should we return early if that happens?
1045 }
1046
1047 try {
@@ -1075,6 +1077,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
1077 const initializedChunk: InitializedChunk<T> = (chunk: any);
1078 initializedChunk.status = INITIALIZED;
1079 initializedChunk.value = value;
1080 + initializedChunk.reason = null;
1081
1082 if (__DEV__) {
1083 processChunkDebugInfo(response, initializedChunk, value);
@@ -1097,6 +1100,7 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
1100 const initializedChunk: InitializedChunk<T> = (chunk: any);
1101 initializedChunk.status = INITIALIZED;
1102 initializedChunk.value = value;
1103 + initializedChunk.reason = null;
1104 } catch (error) {
1105 const erroredChunk: ErroredChunk<T> = (chunk: any);
1106 erroredChunk.status = ERRORED;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+89
@@ -1418,6 +1418,95 @@ describe('ReactFlightDOM', () => {
1418 expect(reportedErrors).toEqual([]);
1419 });
1420
1421 + it('should not retain stale error reason after reentrant module chunk initialization', async () => {
1422 + function MyComponent() {
1423 + return <div>hello from client component</div>;
1424 + }
1425 + const ClientComponent = clientExports(MyComponent);
1426 +
1427 + let resolveAsyncComponent;
1428 + async function AsyncComponent() {
1429 + await new Promise(r => {
1430 + resolveAsyncComponent = r;
1431 + });
1432 + return null;
1433 + }
1434 +
1435 + function ServerComponent() {
1436 + return (
1437 + <>
1438 + <ClientComponent />
1439 + <Suspense>
1440 + <AsyncComponent />
1441 + </Suspense>
1442 + </>
1443 + );
1444 + }
1445 +
1446 + const {writable: flightWritable, readable: flightReadable} =
1447 + getTestStream();
1448 + const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1449 +
1450 + const {pipe} = await serverAct(() =>
1451 + ReactServerDOMServer.renderToPipeableStream(
1452 + <ServerComponent />,
1453 + webpackMap,
1454 + ),
1455 + );
1456 + pipe(flightWritable);
1457 +
1458 + let response = null;
1459 + function getResponse() {
1460 + if (response === null) {
1461 + response =
1462 + ReactServerDOMClient.createFromReadableStream(flightReadable);
1463 + }
1464 + return response;
1465 + }
1466 +
1467 + // Simulate a module that calls captureOwnerStack() during evaluation.
1468 + // In Fizz SSR, this causes a reentrant readChunk on the same module chunk.
1469 + // The reentrant require throws a TDZ error.
1470 + let evaluatingModuleId = null;
1471 + const origRequire = global.__webpack_require__;
1472 + global.__webpack_require__ = function (id) {
1473 + if (id === evaluatingModuleId) {
1474 + throw new ReferenceError(
1475 + "Cannot access 'MyComponent' before initialization",
1476 + );
1477 + }
1478 + const result = origRequire(id);
1479 + if (result === MyComponent) {
1480 + evaluatingModuleId = id;
1481 + if (__DEV__) {
1482 + React.captureOwnerStack();
1483 + }
1484 + evaluatingModuleId = null;
1485 + }
1486 + return result;
1487 + };
1488 +
1489 + function App() {
1490 + return use(getResponse());
1491 + }
1492 +
1493 + await serverAct(async () => {
1494 + ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1495 + });
1496 +
1497 + global.__webpack_require__ = origRequire;
1498 +
1499 + // Resolve the async component so the Flight stream closes after the client
1500 + // module chunk was initialized.
1501 + await serverAct(async () => {
1502 + resolveAsyncComponent();
1503 + });
1504 +
1505 + const container = document.createElement('div');
1506 + await readInto(container, fizzReadable);
1507 + expect(container.innerHTML).toContain('hello from client component');
1508 + });
1509 +
1510 it('should be able to recover from a direct reference erroring server-side', async () => {
1511 const reportedErrors = [];
1512
packages/react-server/src/ReactFlightReplyServer.js
+1
@@ -478,6 +478,7 @@ function loadServerReference<A: Iterable<any>, T>(
478 const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
479 initializedPromise.status = INITIALIZED;
480 initializedPromise.value = resolvedValue;
481 + initializedPromise.reason = null;
482 return resolvedValue;
483 }
484 } else if (bound instanceof ReactPromise) {