mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[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()`.
This commit is contained in:
@@ -1040,6 +1040,8 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
// Initialize any debug info and block the initializing chunk on any
|
||||
// unresolved entries.
|
||||
initializeDebugChunk(response, chunk);
|
||||
// TODO: The chunk might have transitioned to ERRORED now.
|
||||
// Should we return early if that happens?
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1075,6 +1077,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = value;
|
||||
initializedChunk.reason = null;
|
||||
|
||||
if (__DEV__) {
|
||||
processChunkDebugInfo(response, initializedChunk, value);
|
||||
@@ -1097,6 +1100,7 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = value;
|
||||
initializedChunk.reason = null;
|
||||
} catch (error) {
|
||||
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
||||
erroredChunk.status = ERRORED;
|
||||
|
||||
@@ -1418,6 +1418,95 @@ describe('ReactFlightDOM', () => {
|
||||
expect(reportedErrors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not retain stale error reason after reentrant module chunk initialization', async () => {
|
||||
function MyComponent() {
|
||||
return <div>hello from client component</div>;
|
||||
}
|
||||
const ClientComponent = clientExports(MyComponent);
|
||||
|
||||
let resolveAsyncComponent;
|
||||
async function AsyncComponent() {
|
||||
await new Promise(r => {
|
||||
resolveAsyncComponent = r;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
function ServerComponent() {
|
||||
return (
|
||||
<>
|
||||
<ClientComponent />
|
||||
<Suspense>
|
||||
<AsyncComponent />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {writable: flightWritable, readable: flightReadable} =
|
||||
getTestStream();
|
||||
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
|
||||
|
||||
const {pipe} = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToPipeableStream(
|
||||
<ServerComponent />,
|
||||
webpackMap,
|
||||
),
|
||||
);
|
||||
pipe(flightWritable);
|
||||
|
||||
let response = null;
|
||||
function getResponse() {
|
||||
if (response === null) {
|
||||
response =
|
||||
ReactServerDOMClient.createFromReadableStream(flightReadable);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Simulate a module that calls captureOwnerStack() during evaluation.
|
||||
// In Fizz SSR, this causes a reentrant readChunk on the same module chunk.
|
||||
// The reentrant require throws a TDZ error.
|
||||
let evaluatingModuleId = null;
|
||||
const origRequire = global.__webpack_require__;
|
||||
global.__webpack_require__ = function (id) {
|
||||
if (id === evaluatingModuleId) {
|
||||
throw new ReferenceError(
|
||||
"Cannot access 'MyComponent' before initialization",
|
||||
);
|
||||
}
|
||||
const result = origRequire(id);
|
||||
if (result === MyComponent) {
|
||||
evaluatingModuleId = id;
|
||||
if (__DEV__) {
|
||||
React.captureOwnerStack();
|
||||
}
|
||||
evaluatingModuleId = null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function App() {
|
||||
return use(getResponse());
|
||||
}
|
||||
|
||||
await serverAct(async () => {
|
||||
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
|
||||
});
|
||||
|
||||
global.__webpack_require__ = origRequire;
|
||||
|
||||
// Resolve the async component so the Flight stream closes after the client
|
||||
// module chunk was initialized.
|
||||
await serverAct(async () => {
|
||||
resolveAsyncComponent();
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
await readInto(container, fizzReadable);
|
||||
expect(container.innerHTML).toContain('hello from client component');
|
||||
});
|
||||
|
||||
it('should be able to recover from a direct reference erroring server-side', async () => {
|
||||
const reportedErrors = [];
|
||||
|
||||
|
||||
@@ -478,6 +478,7 @@ function loadServerReference<A: Iterable<any>, T>(
|
||||
const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
|
||||
initializedPromise.status = INITIALIZED;
|
||||
initializedPromise.value = resolvedValue;
|
||||
initializedPromise.reason = null;
|
||||
return resolvedValue;
|
||||
}
|
||||
} else if (bound instanceof ReactPromise) {
|
||||
|
||||
Reference in New Issue
Block a user