From 1d163962b21f9396d26930e31719bd8b10d9e107 Mon Sep 17 00:00:00 2001 From: Dennis Kats Date: Sat, 2 Aug 2025 18:11:54 -0400 Subject: [PATCH 1/2] Allow returning a temporary reference inside an async function (#33761) ## Summary Fixes `await`-ing and returning temporary references in `async` functions. These two operations invoke `.then()` under the hood if it is available, which currently results in an "Cannot access then on the server. You cannot dot into a temporary client reference..." error. This can easily be reproduced by returning a temporary reference from a server function. Fixes #33534 ## How did you test this change? I added a test in a new test file. I wasn't sure where else to put it. image --- .../ReactFlightServerTemporaryReferences.js | 3 ++ ...actFlightServerTemporaryReferences-test.js | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js index e368b2e800795..e28b603edf9b5 100644 --- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js +++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js @@ -70,6 +70,9 @@ const proxyHandlers = { `Instead, you can export a Client Component wrapper ` + `that itself renders a Client Context Provider.`, ); + // Allow returning a temporary reference from an async function + case 'then': + return undefined; } throw new Error( // eslint-disable-next-line react-internal/safe-string-coercion diff --git a/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js b/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js new file mode 100644 index 0000000000000..7e93179268d50 --- /dev/null +++ b/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +let ReactFlightServerTemporaryReferences; + +describe('ReactFlightServerTemporaryReferences', () => { + beforeEach(() => { + jest.resetModules(); + ReactFlightServerTemporaryReferences = require('react-server/src/ReactFlightServerTemporaryReferences'); + }); + + it('can return a temporary reference from an async function', async () => { + const temporaryReferenceSet = + ReactFlightServerTemporaryReferences.createTemporaryReferenceSet(); + const temporaryReference = + ReactFlightServerTemporaryReferences.createTemporaryReference( + temporaryReferenceSet, + 'test', + ); + + async function foo() { + return temporaryReference; + } + + await expect(foo()).resolves.toBe(temporaryReference); + }); +}); From c499adf8c89bbfd884f4d3a58c4e510001383525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 2 Aug 2025 18:44:20 -0400 Subject: [PATCH 2/2] [Flight] Allow Temporary References to be awaited (#34084) Fixes #33534. `.then` method can be tested when you await a value that's not a Promise. For regular Client References we have a way to mark those as "async" and yield a reference to the unwrapped value in case it's a Promise on the Client. However, the realization is that we never serialize Promises as opaque when passed from the client to the server. If a Promise is passed, then it would've been deserialized as a Promise (while still registered as a temporary reference) and not one of these Proxy objects. Technically it could be a non-function value on the client which would be wrong but you're not supposed to dot into it in the first place. So we can just assume it's `undefined`. --- .../src/__tests__/ReactFlightDOMReply-test.js | 44 +++++++++++++++++++ .../ReactFlightServerTemporaryReferences.js | 6 ++- ...actFlightServerTemporaryReferences-test.js | 36 --------------- 3 files changed, 49 insertions(+), 37 deletions(-) delete mode 100644 packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 6e113556206f4..2181eb5fe700c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -438,6 +438,50 @@ describe('ReactFlightDOMReply', () => { expect(response.obj).toBe(obj); }); + it('can return an opaque object through an async function', async () => { + function fn() { + return 'this is a client function'; + } + + const args = [fn]; + + const temporaryReferences = + ReactServerDOMClient.createTemporaryReferenceSet(); + const body = await ReactServerDOMClient.encodeReply(args, { + temporaryReferences, + }); + + const temporaryReferencesServer = + ReactServerDOMServer.createTemporaryReferenceSet(); + const serverPayload = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + {temporaryReferences: temporaryReferencesServer}, + ); + + async function action(arg) { + return arg; + } + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + { + result: action.apply(null, serverPayload), + }, + null, + {temporaryReferences: temporaryReferencesServer}, + ), + ); + const response = await ReactServerDOMClient.createFromReadableStream( + stream, + { + temporaryReferences, + }, + ); + + expect(await response.result).toBe(fn); + }); + it('should supports streaming ReadableStream with objects', async () => { let controller1; let controller2; diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js index e28b603edf9b5..1ea89c81980c1 100644 --- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js +++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js @@ -70,8 +70,12 @@ const proxyHandlers = { `Instead, you can export a Client Component wrapper ` + `that itself renders a Client Context Provider.`, ); - // Allow returning a temporary reference from an async function case 'then': + // Allow returning a temporary reference from an async function + // Unlike regular Client References, a Promise would never have been serialized as + // an opaque Temporary Reference, but instead would have been serialized as a + // Promise on the server and so doesn't hit this path. So we can assume this wasn't + // a Promise on the client. return undefined; } throw new Error( diff --git a/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js b/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js deleted file mode 100644 index 7e93179268d50..0000000000000 --- a/packages/react-server/src/__tests__/ReactFlightServerTemporaryReferences-test.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - * @jest-environment node - */ - -'use strict'; - -let ReactFlightServerTemporaryReferences; - -describe('ReactFlightServerTemporaryReferences', () => { - beforeEach(() => { - jest.resetModules(); - ReactFlightServerTemporaryReferences = require('react-server/src/ReactFlightServerTemporaryReferences'); - }); - - it('can return a temporary reference from an async function', async () => { - const temporaryReferenceSet = - ReactFlightServerTemporaryReferences.createTemporaryReferenceSet(); - const temporaryReference = - ReactFlightServerTemporaryReferences.createTemporaryReference( - temporaryReferenceSet, - 'test', - ); - - async function foo() { - return temporaryReference; - } - - await expect(foo()).resolves.toBe(temporaryReference); - }); -});