[Flight] Fix debug info leaking to outer handler (#34081)

The `waitForReference` call for debug info can trigger inside a
different object's initializingHandler. In that case, we can get
confused by which one is the root object.

We have this special case to detect if the initializing handler's object
is `null` and we have an empty string key, then we should replace the
root object's value with the resolved value.


52612a7cbd/packages/react-client/src/ReactFlightClient.js (L1374)

However, if the initializing handler actually should have the value
`null` then we might get confused by this and replace it with the
resolved value from a debug object. This fixes it by just using a
non-empty string as the key for the waitForReference on debug value
since we're not going to use it anyway.

It used to be impossible to get into this state since a `null` value at
the root couldn't have any reference inside itself but now the debug
info for a `null` value can have outstanding references.

However, a better fix might be using a placeholder marker object instead
of null or better yet ensuring that we know which root we're
initializing in the debug model.
This commit is contained in:
Sebastian Markbåge 2025-08-01 15:44:48 -04:00 committed by GitHub
parent 52612a7cbd
commit 538ac7ae4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 1 deletions

View File

@ -879,7 +879,7 @@ function initializeDebugChunk(
waitForReference(
debugChunk,
{}, // noop, since we'll have already added an entry to debug info
'', // noop
'debug', // noop, but we need it to not be empty string since that indicates the root object
response,
initializeDebugInfo,
[''], // path

View File

@ -1915,4 +1915,57 @@ describe('ReactFlightDOMEdge', () => {
expect(ownerStack).toBeNull();
}
});
it('can pass an async import that resolves later as a prop to a null component', async () => {
let resolveClientComponentChunk;
const client = clientExports(
{
foo: 'bar',
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function ServerComponent(props) {
return null;
}
function App() {
return (
<div>
<ServerComponent client={client} />
</div>
);
}
const stream = await serverAct(() =>
passThrough(
ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
),
);
// Parsing the root blocks because the module hasn't loaded yet
const response = ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
});
function ClientRoot() {
return use(response);
}
// Initialize to be blocked.
response.then(() => {});
// Unblock.
resolveClientComponentChunk();
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual('<div></div>');
});
});