From 9ceb1e7d9e20bd0302cf6ab31b038c5ec673178d Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 27 Jul 2026 15:30:19 -0400 Subject: [PATCH] [Flight] Define Flight chunk `.then` with `Object.defineProperty` (#37109) [Secure Ecmascript](https://github.com/tc39/proposal-ses) would freeze the prototype of intrinsics. Since `ReactPromise` inherits the prototype from `Promise`, it also copies over the writable definition. Using `defineProperty` on an inherited property is compatible with SES though. That's also closer to how classes are specced in JS. --------- Co-authored-by: Claude Fable 5 --- .../react-client/src/ReactFlightClient.js | 14 ++++- .../ReactFlightNonWritablePromiseThen-test.js | 56 +++++++++++++++++++ .../src/ReactFlightReplyServer.js | 14 ++++- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 packages/react-server-dom-webpack/src/__tests__/ReactFlightNonWritablePromiseThen-test.js diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 348132c7f25..14936d48e2b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -256,7 +256,7 @@ function ReactPromise(status: any, value: any, reason: any) { // We subclass Promise.prototype so that we get other methods like .catch ReactPromise.prototype = Object.create(Promise.prototype) as any; // TODO: This doesn't return a new Promise chain unlike the real .then -ReactPromise.prototype.then = function ( +function reactPromiseThen( this: SomeChunk, resolve: (value: T) => mixed, reject?: (reason: mixed) => mixed, @@ -326,7 +326,17 @@ ReactPromise.prototype.then = function ( } break; } -}; +} +// The shadowing `then` must be defined with `Object.defineProperty` instead of +// assignment. Assignment would throw when `Promise.prototype` is frozen (e.g. +// by SES lockdown) because assigning over an inherited non-writable property +// is rejected. +Object.defineProperty(ReactPromise.prototype, 'then', { + writable: true, + enumerable: true, + configurable: true, + value: reactPromiseThen, +}); export type FindSourceMapURLCallback = ( fileName: string, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightNonWritablePromiseThen-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightNonWritablePromiseThen-test.js new file mode 100644 index 00000000000..7a8ebd64146 --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightNonWritablePromiseThen-test.js @@ -0,0 +1,56 @@ +/** + * 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 + */ + +'use strict'; + +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +describe('ReactFlight with a non-writable Promise.prototype.then', () => { + let originalThen; + + beforeEach(() => { + jest.resetModules(); + originalThen = Object.getOwnPropertyDescriptor(Promise.prototype, 'then'); + // eslint-disable-next-line no-extend-native + Object.defineProperty(Promise.prototype, 'then', { + ...originalThen, + writable: false, + }); + }); + + afterEach(() => { + // eslint-disable-next-line no-extend-native + Object.defineProperty(Promise.prototype, 'then', originalThen); + }); + + it('can require Server and Client entrypoints', () => { + patchMessageChannel(require('scheduler')); + // Simulate the condition resolution + jest.mock('react', () => require('react/react.react-server')); + jest.mock('react-server-dom-webpack/server', () => + require('react-server-dom-webpack/server.browser'), + ); + require('./utils/WebpackMock'); + const ReactServerDOMServer = require('react-server-dom-webpack/server'); + expect(typeof ReactServerDOMServer.decodeReply).toBe('function'); + + __unmockReact(); + jest.resetModules(); + const ReactServerDOMClient = require('react-server-dom-webpack/client'); + expect(typeof ReactServerDOMClient.createFromReadableStream).toBe( + 'function', + ); + }); +}); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 866584ff82a..aa4ef53fa38 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -126,7 +126,7 @@ function ReactPromise(status: any, value: any, reason: any) { // We subclass Promise.prototype so that we get other methods like .catch ReactPromise.prototype = Object.create(Promise.prototype) as any; // TODO: This doesn't return a new Promise chain unlike the real .then -ReactPromise.prototype.then = function ( +function reactPromiseThen( this: SomeChunk, resolve: (value: T) => mixed, reject: ?(reason: mixed) => mixed, @@ -197,7 +197,17 @@ ReactPromise.prototype.then = function ( } break; } -}; +} +// The shadowing `then` must be defined with `Object.defineProperty` instead of +// assignment. Assignment would throw when `Promise.prototype` is frozen (e.g. +// by SES lockdown) because assigning over an inherited non-writable property +// is rejected. +Object.defineProperty(ReactPromise.prototype, 'then', { + writable: true, + enumerable: true, + configurable: true, + value: reactPromiseThen, +}); const ObjectPrototype = Object.prototype; const ArrayPrototype = Array.prototype;