[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;