@samitouri / QOS-React-1 / commits / 9ceb1e7d9e

[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 <noreply@anthropic.com>

Sebastian "Sebbie" Silbermann committed Jul 27, 2026 at 15:30 UTC 9ceb1e7d9e20bd0302cf6ab31b038c5ec673178d
3 files changed +80 -4
packages/react-client/src/ReactFlightClient.js
+12 -2
@@ -256,7 +256,7 @@ function ReactPromise(status: any, value: any, reason: any) {
256 // We subclass Promise.prototype so that we get other methods like .catch
257 ReactPromise.prototype = Object.create(Promise.prototype) as any;
258 // TODO: This doesn't return a new Promise chain unlike the real .then
259 -ReactPromise.prototype.then = function <T>(
259 +function reactPromiseThen<T>(
260 this: SomeChunk<T>,
261 resolve: (value: T) => mixed,
262 reject?: (reason: mixed) => mixed,
@@ -326,7 +326,17 @@ ReactPromise.prototype.then = function <T>(
326 }
327 break;
328 }
329 -};
329 +}
330 +// The shadowing `then` must be defined with `Object.defineProperty` instead of
331 +// assignment. Assignment would throw when `Promise.prototype` is frozen (e.g.
332 +// by SES lockdown) because assigning over an inherited non-writable property
333 +// is rejected.
334 +Object.defineProperty(ReactPromise.prototype, 'then', {
335 + writable: true,
336 + enumerable: true,
337 + configurable: true,
338 + value: reactPromiseThen,
339 +});
340
341 export type FindSourceMapURLCallback = (
342 fileName: string,
packages/react-server-dom-webpack/src/__tests__/ReactFlightNonWritablePromiseThen-test.js new
+56
@@ -0,0 +1,56 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
13 +
14 +// Polyfills for test environment
15 +global.ReadableStream =
16 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17 +global.TextEncoder = require('util').TextEncoder;
18 +global.TextDecoder = require('util').TextDecoder;
19 +
20 +describe('ReactFlight with a non-writable Promise.prototype.then', () => {
21 + let originalThen;
22 +
23 + beforeEach(() => {
24 + jest.resetModules();
25 + originalThen = Object.getOwnPropertyDescriptor(Promise.prototype, 'then');
26 + // eslint-disable-next-line no-extend-native
27 + Object.defineProperty(Promise.prototype, 'then', {
28 + ...originalThen,
29 + writable: false,
30 + });
31 + });
32 +
33 + afterEach(() => {
34 + // eslint-disable-next-line no-extend-native
35 + Object.defineProperty(Promise.prototype, 'then', originalThen);
36 + });
37 +
38 + it('can require Server and Client entrypoints', () => {
39 + patchMessageChannel(require('scheduler'));
40 + // Simulate the condition resolution
41 + jest.mock('react', () => require('react/react.react-server'));
42 + jest.mock('react-server-dom-webpack/server', () =>
43 + require('react-server-dom-webpack/server.browser'),
44 + );
45 + require('./utils/WebpackMock');
46 + const ReactServerDOMServer = require('react-server-dom-webpack/server');
47 + expect(typeof ReactServerDOMServer.decodeReply).toBe('function');
48 +
49 + __unmockReact();
50 + jest.resetModules();
51 + const ReactServerDOMClient = require('react-server-dom-webpack/client');
52 + expect(typeof ReactServerDOMClient.createFromReadableStream).toBe(
53 + 'function',
54 + );
55 + });
56 +});
packages/react-server/src/ReactFlightReplyServer.js
+12 -2
@@ -126,7 +126,7 @@ function ReactPromise(status: any, value: any, reason: any) {
126 // We subclass Promise.prototype so that we get other methods like .catch
127 ReactPromise.prototype = Object.create(Promise.prototype) as any;
128 // TODO: This doesn't return a new Promise chain unlike the real .then
129 -ReactPromise.prototype.then = function <T>(
129 +function reactPromiseThen<T>(
130 this: SomeChunk<T>,
131 resolve: (value: T) => mixed,
132 reject: ?(reason: mixed) => mixed,
@@ -197,7 +197,17 @@ ReactPromise.prototype.then = function <T>(
197 }
198 break;
199 }
200 -};
200 +}
201 +// The shadowing `then` must be defined with `Object.defineProperty` instead of
202 +// assignment. Assignment would throw when `Promise.prototype` is frozen (e.g.
203 +// by SES lockdown) because assigning over an inherited non-writable property
204 +// is rejected.
205 +Object.defineProperty(ReactPromise.prototype, 'then', {
206 + writable: true,
207 + enumerable: true,
208 + configurable: true,
209 + value: reactPromiseThen,
210 +});
211
212 const ObjectPrototype = Object.prototype;
213 const ArrayPrototype = Array.prototype;