@samitouri / QOS-React-1 / commits / 538ac7ae4b

[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. https://github.com/facebook/react/blob/52612a7cbdd8e1fee9599478247f78725869ebad/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.

Sebastian Markbåge committed Aug 1, 2025 at 15:44 UTC 538ac7ae4b918136c5bd7d15ada19439d15f8080
2 files changed +54 -1
packages/react-client/src/ReactFlightClient.js
+1 -1
@@ -879,7 +879,7 @@ function initializeDebugChunk(
879 waitForReference(
880 debugChunk,
881 {}, // noop, since we'll have already added an entry to debug info
882 - '', // noop
882 + 'debug', // noop, but we need it to not be empty string since that indicates the root object
883 response,
884 initializeDebugInfo,
885 [''], // path
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+53
@@ -1915,4 +1915,57 @@ describe('ReactFlightDOMEdge', () => {
1915 expect(ownerStack).toBeNull();
1916 }
1917 });
1918 +
1919 + it('can pass an async import that resolves later as a prop to a null component', async () => {
1920 + let resolveClientComponentChunk;
1921 + const client = clientExports(
1922 + {
1923 + foo: 'bar',
1924 + },
1925 + '42',
1926 + '/test.js',
1927 + new Promise(resolve => (resolveClientComponentChunk = resolve)),
1928 + );
1929 +
1930 + function ServerComponent(props) {
1931 + return null;
1932 + }
1933 +
1934 + function App() {
1935 + return (
1936 + <div>
1937 + <ServerComponent client={client} />
1938 + </div>
1939 + );
1940 + }
1941 +
1942 + const stream = await serverAct(() =>
1943 + passThrough(
1944 + ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
1945 + ),
1946 + );
1947 +
1948 + // Parsing the root blocks because the module hasn't loaded yet
1949 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
1950 + serverConsumerManifest: {
1951 + moduleMap: null,
1952 + moduleLoading: null,
1953 + },
1954 + });
1955 +
1956 + function ClientRoot() {
1957 + return use(response);
1958 + }
1959 +
1960 + // Initialize to be blocked.
1961 + response.then(() => {});
1962 + // Unblock.
1963 + resolveClientComponentChunk();
1964 +
1965 + const ssrStream = await serverAct(() =>
1966 + ReactDOMServer.renderToReadableStream(<ClientRoot />),
1967 + );
1968 + const result = await readResult(ssrStream);
1969 + expect(result).toEqual('<div></div>');
1970 + });
1971 });