[Flight] Add more DoS mitigations to Flight Reply, and harden Flight (#35632)
This fixes security vulnerabilities in Server Functions. --------- Co-authored-by: Sebastian Markbåge <sebastian@calyptus.eu> Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
Hendrik Liebau committed
Jan 26, 2026 at 20:24 UTC
10680271fab565e0edf948d3a6dc9d30e83df94c
18 files changed
+835
-263
packages/react-client/src/ReactFlightClient.js
+57
-33
@@ -94,6 +94,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
94
95
import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack';
96
97
+import hasOwnProperty from 'shared/hasOwnProperty';
98
+
99
import {injectInternals} from './ReactFlightClientDevToolsHook';
100
101
import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
@@ -159,6 +161,8 @@ const INITIALIZED = 'fulfilled';
161
const ERRORED = 'rejected';
162
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
163
164
+const __PROTO__ = '__proto__';
165
+
166
type PendingChunk<T> = {
167
status: 'pending',
168
value: null | Array<InitializationReference | (T => mixed)>,
@@ -1544,7 +1548,16 @@ function fulfillReference(
1548
}
1549
}
1550
}
1547
- value = value[path[i]];
1551
+ const name = path[i];
1552
+ if (
1553
+ typeof value === 'object' &&
1554
+ value !== null &&
1555
+ hasOwnProperty.call(value, name)
1556
+ ) {
1557
+ value = value[name];
1558
+ } else {
1559
+ throw new Error('Invalid reference.');
1560
+ }
1561
}
1562
1563
while (
@@ -1580,7 +1593,9 @@ function fulfillReference(
1593
}
1594
1595
const mappedValue = map(response, value, parentObject, key);
1583
- parentObject[key] = mappedValue;
1596
+ if (key !== __PROTO__) {
1597
+ parentObject[key] = mappedValue;
1598
+ }
1599
1600
// If this is the root object for a model reference, where `handler.value`
1601
// is a stale `null`, the resolved value can be used directly.
@@ -1849,7 +1864,9 @@ function loadServerReference<A: Iterable<any>, T>(
1864
response._encodeFormAction,
1865
);
1866
1852
- parentObject[key] = resolvedValue;
1867
+ if (key !== __PROTO__) {
1868
+ parentObject[key] = resolvedValue;
1869
+ }
1870
1871
// If this is the root object for a model reference, where `handler.value`
1872
// is a stale `null`, the resolved value can be used directly.
@@ -2231,29 +2248,31 @@ function defineLazyGetter<T>(
2248
): any {
2249
// We don't immediately initialize it even if it's resolved.
2250
// Instead, we wait for the getter to get accessed.
2234
- Object.defineProperty(parentObject, key, {
2235
- get: function () {
2236
- if (chunk.status === RESOLVED_MODEL) {
2237
- // If it was now resolved, then we initialize it. This may then discover
2238
- // a new set of lazy references that are then asked for eagerly in case
2239
- // we get that deep.
2240
- initializeModelChunk(chunk);
2241
- }
2242
- switch (chunk.status) {
2243
- case INITIALIZED: {
2244
- return chunk.value;
2251
+ if (key !== __PROTO__) {
2252
+ Object.defineProperty(parentObject, key, {
2253
+ get: function () {
2254
+ if (chunk.status === RESOLVED_MODEL) {
2255
+ // If it was now resolved, then we initialize it. This may then discover
2256
+ // a new set of lazy references that are then asked for eagerly in case
2257
+ // we get that deep.
2258
+ initializeModelChunk(chunk);
2259
}
2246
- case ERRORED:
2247
- throw chunk.reason;
2248
- }
2249
- // Otherwise, we didn't have enough time to load the object before it was
2250
- // accessed or the connection closed. So we just log that it was omitted.
2251
- // TODO: We should ideally throw here to indicate a difference.
2252
- return OMITTED_PROP_ERROR;
2253
- },
2254
- enumerable: true,
2255
- configurable: false,
2256
- });
2260
+ switch (chunk.status) {
2261
+ case INITIALIZED: {
2262
+ return chunk.value;
2263
+ }
2264
+ case ERRORED:
2265
+ throw chunk.reason;
2266
+ }
2267
+ // Otherwise, we didn't have enough time to load the object before it was
2268
+ // accessed or the connection closed. So we just log that it was omitted.
2269
+ // TODO: We should ideally throw here to indicate a difference.
2270
+ return OMITTED_PROP_ERROR;
2271
+ },
2272
+ enumerable: true,
2273
+ configurable: false,
2274
+ });
2275
+ }
2276
return null;
2277
}
2278
@@ -2564,14 +2583,16 @@ function parseModelString(
2583
// In DEV mode we encode omitted objects in logs as a getter that throws
2584
// so that when you try to access it on the client, you know why that
2585
// happened.
2567
- Object.defineProperty(parentObject, key, {
2568
- get: function () {
2569
- // TODO: We should ideally throw here to indicate a difference.
2570
- return OMITTED_PROP_ERROR;
2571
- },
2572
- enumerable: true,
2573
- configurable: false,
2574
- });
2586
+ if (key !== __PROTO__) {
2587
+ Object.defineProperty(parentObject, key, {
2588
+ get: function () {
2589
+ // TODO: We should ideally throw here to indicate a difference.
2590
+ return OMITTED_PROP_ERROR;
2591
+ },
2592
+ enumerable: true,
2593
+ configurable: false,
2594
+ });
2595
+ }
2596
return null;
2597
}
2598
// Fallthrough
@@ -5183,6 +5204,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
5204
function createFromJSONCallback(response: Response) {
5205
// $FlowFixMe[missing-this-annot]
5206
return function (key: string, value: JSONValue) {
5207
+ if (key === __PROTO__) {
5208
+ return undefined;
5209
+ }
5210
if (typeof value === 'string') {
5211
// We can't use .bind here because we need the "this" value.
5212
return parseModelString(response, this, key, value);
packages/react-client/src/ReactFlightReplyClient.js
+19
-1
@@ -95,6 +95,8 @@ export type ReactServerValue =
95
96
type ReactServerObject = {+[key: string]: ReactServerValue};
97
98
+const __PROTO__ = '__proto__';
99
+
100
function serializeByValueID(id: number): string {
101
return '$' + id.toString(16);
102
}
@@ -361,6 +363,15 @@ export function processReply(
363
): ReactJSONValue {
364
const parent = this;
365
366
+ if (__DEV__) {
367
+ if (key === __PROTO__) {
368
+ console.error(
369
+ 'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370
+ describeObjectForErrorMessage(parent, key),
371
+ );
372
+ }
373
+ }
374
+
375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
376
if (__DEV__) {
377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
791
if (typeof value === 'function') {
792
const referenceClosure = knownServerReferences.get(value);
793
if (referenceClosure !== undefined) {
794
+ const existingReference = writtenObjects.get(value);
795
+ if (existingReference !== undefined) {
796
+ return existingReference;
797
+ }
798
const {id, bound} = referenceClosure;
799
const referenceClosureJSON = JSON.stringify({id, bound}, resolveToJSON);
800
if (formData === null) {
@@ -789,7 +804,10 @@ export function processReply(
804
// The reference to this function came from the same client so we can pass it back.
805
const refId = nextPartId++;
806
formData.set(formFieldPrefix + refId, referenceClosureJSON);
792
- return serializeServerReferenceID(refId);
807
+ const serverReferenceId = serializeServerReferenceID(refId);
808
+ // Store the server reference ID for deduplication.
809
+ writtenObjects.set(value, serverReferenceId);
810
+ return serverReferenceId;
811
}
812
if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.
packages/react-client/src/forks/ReactFlightClientConfig.markup.js
+1
-1
@@ -43,7 +43,7 @@ export function resolveClientReference<T>(
43
44
export function resolveServerReference<T>(
45
config: ServerManifest,
46
- id: ServerReferenceId,
46
+ id: mixed,
47
): ClientReference<T> {
48
throw new Error(
49
'renderToHTML should not have emitted Server References. This is a bug in React.',
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+11
-2
@@ -328,12 +328,17 @@ function prerenderToNodeStream(
328
function decodeReplyFromBusboy<T>(
329
busboyStream: Busboy,
330
moduleBasePath: ServerManifest,
331
- options?: {temporaryReferences?: TemporaryReferenceSet},
331
+ options?: {
332
+ temporaryReferences?: TemporaryReferenceSet,
333
+ arraySizeLimit?: number,
334
+ },
335
): Thenable<T> {
336
const response = createResponse(
337
moduleBasePath,
338
'',
339
options ? options.temporaryReferences : undefined,
340
+ undefined,
341
+ options ? options.arraySizeLimit : undefined,
342
);
343
let pendingFiles = 0;
344
const queuedFields: Array<string> = [];
@@ -399,7 +404,10 @@ function decodeReplyFromBusboy<T>(
404
function decodeReply<T>(
405
body: string | FormData,
406
moduleBasePath: ServerManifest,
402
- options?: {temporaryReferences?: TemporaryReferenceSet},
407
+ options?: {
408
+ temporaryReferences?: TemporaryReferenceSet,
409
+ arraySizeLimit?: number,
410
+ },
411
): Thenable<T> {
412
if (typeof body === 'string') {
413
const form = new FormData();
@@ -411,6 +419,7 @@ function decodeReply<T>(
419
'',
420
options ? options.temporaryReferences : undefined,
421
body,
422
+ options ? options.arraySizeLimit : undefined,
423
);
424
const root = getRoot<T>(response);
425
close(response);
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
+5
-1
@@ -245,7 +245,10 @@ export function registerServerActions(manifest: ServerManifest) {
245
246
export function decodeReply<T>(
247
body: string | FormData,
248
- options?: {temporaryReferences?: TemporaryReferenceSet},
248
+ options?: {
249
+ temporaryReferences?: TemporaryReferenceSet,
250
+ arraySizeLimit?: number,
251
+ },
252
): Thenable<T> {
253
if (typeof body === 'string') {
254
const form = new FormData();
@@ -257,6 +260,7 @@ export function decodeReply<T>(
260
'',
261
options ? options.temporaryReferences : undefined,
262
body,
263
+ options ? options.arraySizeLimit : undefined,
264
);
265
const root = getRoot<T>(response);
266
close(response);
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
+5
-1
@@ -250,7 +250,10 @@ export function registerServerActions(manifest: ServerManifest) {
250
251
export function decodeReply<T>(
252
body: string | FormData,
253
- options?: {temporaryReferences?: TemporaryReferenceSet},
253
+ options?: {
254
+ temporaryReferences?: TemporaryReferenceSet,
255
+ arraySizeLimit?: number,
256
+ },
257
): Thenable<T> {
258
if (typeof body === 'string') {
259
const form = new FormData();
@@ -262,6 +265,7 @@ export function decodeReply<T>(
265
'',
266
options ? options.temporaryReferences : undefined,
267
body,
268
+ options ? options.arraySizeLimit : undefined,
269
);
270
const root = getRoot<T>(response);
271
close(response);
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+17
-3
@@ -556,12 +556,17 @@ export function registerServerActions(manifest: ServerManifest) {
556
557
export function decodeReplyFromBusboy<T>(
558
busboyStream: Busboy,
559
- options?: {temporaryReferences?: TemporaryReferenceSet},
559
+ options?: {
560
+ temporaryReferences?: TemporaryReferenceSet,
561
+ arraySizeLimit?: number,
562
+ },
563
): Thenable<T> {
564
const response = createResponse(
565
serverManifest,
566
'',
567
options ? options.temporaryReferences : undefined,
568
+ undefined,
569
+ options ? options.arraySizeLimit : undefined,
570
);
571
let pendingFiles = 0;
572
const queuedFields: Array<string> = [];
@@ -626,7 +631,10 @@ export function decodeReplyFromBusboy<T>(
631
632
export function decodeReply<T>(
633
body: string | FormData,
629
- options?: {temporaryReferences?: TemporaryReferenceSet},
634
+ options?: {
635
+ temporaryReferences?: TemporaryReferenceSet,
636
+ arraySizeLimit?: number,
637
+ },
638
): Thenable<T> {
639
if (typeof body === 'string') {
640
const form = new FormData();
@@ -638,6 +646,7 @@ export function decodeReply<T>(
646
'',
647
options ? options.temporaryReferences : undefined,
648
body,
649
+ options ? options.arraySizeLimit : undefined,
650
);
651
const root = getRoot<T>(response);
652
close(response);
@@ -646,7 +655,10 @@ export function decodeReply<T>(
655
656
export function decodeReplyFromAsyncIterable<T>(
657
iterable: AsyncIterable<[string, string | File]>,
649
- options?: {temporaryReferences?: TemporaryReferenceSet},
658
+ options?: {
659
+ temporaryReferences?: TemporaryReferenceSet,
660
+ arraySizeLimit?: number,
661
+ },
662
): Thenable<T> {
663
const iterator: AsyncIterator<[string, string | File]> =
664
iterable[ASYNC_ITERATOR]();
@@ -655,6 +667,8 @@ export function decodeReplyFromAsyncIterable<T>(
667
serverManifest,
668
'',
669
options ? options.temporaryReferences : undefined,
670
+ undefined,
671
+ options ? options.arraySizeLimit : undefined,
672
);
673
674
function progress(
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+5
-1
@@ -239,7 +239,10 @@ function prerender(
239
function decodeReply<T>(
240
body: string | FormData,
241
turbopackMap: ServerManifest,
242
- options?: {temporaryReferences?: TemporaryReferenceSet},
242
+ options?: {
243
+ temporaryReferences?: TemporaryReferenceSet,
244
+ arraySizeLimit?: number,
245
+ },
246
): Thenable<T> {
247
if (typeof body === 'string') {
248
const form = new FormData();
@@ -251,6 +254,7 @@ function decodeReply<T>(
254
'',
255
options ? options.temporaryReferences : undefined,
256
body,
257
+ options ? options.arraySizeLimit : undefined,
258
);
259
const root = getRoot<T>(response);
260
close(response);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+11
-2
@@ -244,7 +244,10 @@ function prerender(
244
function decodeReply<T>(
245
body: string | FormData,
246
turbopackMap: ServerManifest,
247
- options?: {temporaryReferences?: TemporaryReferenceSet},
247
+ options?: {
248
+ temporaryReferences?: TemporaryReferenceSet,
249
+ arraySizeLimit?: number,
250
+ },
251
): Thenable<T> {
252
if (typeof body === 'string') {
253
const form = new FormData();
@@ -256,6 +259,7 @@ function decodeReply<T>(
259
'',
260
options ? options.temporaryReferences : undefined,
261
body,
262
+ options ? options.arraySizeLimit : undefined,
263
);
264
const root = getRoot<T>(response);
265
close(response);
@@ -265,7 +269,10 @@ function decodeReply<T>(
269
function decodeReplyFromAsyncIterable<T>(
270
iterable: AsyncIterable<[string, string | File]>,
271
turbopackMap: ServerManifest,
268
- options?: {temporaryReferences?: TemporaryReferenceSet},
272
+ options?: {
273
+ temporaryReferences?: TemporaryReferenceSet,
274
+ arraySizeLimit?: number,
275
+ },
276
): Thenable<T> {
277
const iterator: AsyncIterator<[string, string | File]> =
278
iterable[ASYNC_ITERATOR]();
@@ -274,6 +281,8 @@ function decodeReplyFromAsyncIterable<T>(
281
turbopackMap,
282
'',
283
options ? options.temporaryReferences : undefined,
284
+ undefined,
285
+ options ? options.arraySizeLimit : undefined,
286
);
287
288
function progress(
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+17
-3
@@ -548,12 +548,17 @@ function prerender(
548
function decodeReplyFromBusboy<T>(
549
busboyStream: Busboy,
550
turbopackMap: ServerManifest,
551
- options?: {temporaryReferences?: TemporaryReferenceSet},
551
+ options?: {
552
+ temporaryReferences?: TemporaryReferenceSet,
553
+ arraySizeLimit?: number,
554
+ },
555
): Thenable<T> {
556
const response = createResponse(
557
turbopackMap,
558
'',
559
options ? options.temporaryReferences : undefined,
560
+ undefined,
561
+ options ? options.arraySizeLimit : undefined,
562
);
563
let pendingFiles = 0;
564
const queuedFields: Array<string> = [];
@@ -619,7 +624,10 @@ function decodeReplyFromBusboy<T>(
624
function decodeReply<T>(
625
body: string | FormData,
626
turbopackMap: ServerManifest,
622
- options?: {temporaryReferences?: TemporaryReferenceSet},
627
+ options?: {
628
+ temporaryReferences?: TemporaryReferenceSet,
629
+ arraySizeLimit?: number,
630
+ },
631
): Thenable<T> {
632
if (typeof body === 'string') {
633
const form = new FormData();
@@ -631,6 +639,7 @@ function decodeReply<T>(
639
'',
640
options ? options.temporaryReferences : undefined,
641
body,
642
+ options ? options.arraySizeLimit : undefined,
643
);
644
const root = getRoot<T>(response);
645
close(response);
@@ -640,7 +649,10 @@ function decodeReply<T>(
649
function decodeReplyFromAsyncIterable<T>(
650
iterable: AsyncIterable<[string, string | File]>,
651
turbopackMap: ServerManifest,
643
- options?: {temporaryReferences?: TemporaryReferenceSet},
652
+ options?: {
653
+ temporaryReferences?: TemporaryReferenceSet,
654
+ arraySizeLimit?: number,
655
+ },
656
): Thenable<T> {
657
const iterator: AsyncIterator<[string, string | File]> =
658
iterable[ASYNC_ITERATOR]();
@@ -649,6 +661,8 @@ function decodeReplyFromAsyncIterable<T>(
661
turbopackMap,
662
'',
663
options ? options.temporaryReferences : undefined,
664
+ undefined,
665
+ options ? options.arraySizeLimit : undefined,
666
);
667
668
function progress(
packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js
+17
-3
@@ -548,12 +548,17 @@ function prerender(
548
function decodeReplyFromBusboy<T>(
549
busboyStream: Busboy,
550
webpackMap: ServerManifest,
551
- options?: {temporaryReferences?: TemporaryReferenceSet},
551
+ options?: {
552
+ temporaryReferences?: TemporaryReferenceSet,
553
+ arraySizeLimit?: number,
554
+ },
555
): Thenable<T> {
556
const response = createResponse(
557
webpackMap,
558
'',
559
options ? options.temporaryReferences : undefined,
560
+ undefined,
561
+ options ? options.arraySizeLimit : undefined,
562
);
563
let pendingFiles = 0;
564
const queuedFields: Array<string> = [];
@@ -619,7 +624,10 @@ function decodeReplyFromBusboy<T>(
624
function decodeReply<T>(
625
body: string | FormData,
626
webpackMap: ServerManifest,
622
- options?: {temporaryReferences?: TemporaryReferenceSet},
627
+ options?: {
628
+ temporaryReferences?: TemporaryReferenceSet,
629
+ arraySizeLimit?: number,
630
+ },
631
): Thenable<T> {
632
if (typeof body === 'string') {
633
const form = new FormData();
@@ -631,6 +639,7 @@ function decodeReply<T>(
639
'',
640
options ? options.temporaryReferences : undefined,
641
body,
642
+ options ? options.arraySizeLimit : undefined,
643
);
644
const root = getRoot<T>(response);
645
close(response);
@@ -640,7 +649,10 @@ function decodeReply<T>(
649
function decodeReplyFromAsyncIterable<T>(
650
iterable: AsyncIterable<[string, string | File]>,
651
webpackMap: ServerManifest,
643
- options?: {temporaryReferences?: TemporaryReferenceSet},
652
+ options?: {
653
+ temporaryReferences?: TemporaryReferenceSet,
654
+ arraySizeLimit?: number,
655
+ },
656
): Thenable<T> {
657
const iterator: AsyncIterator<[string, string | File]> =
658
iterable[ASYNC_ITERATOR]();
@@ -649,6 +661,8 @@ function decodeReplyFromAsyncIterable<T>(
661
webpackMap,
662
'',
663
options ? options.temporaryReferences : undefined,
664
+ undefined,
665
+ options ? options.arraySizeLimit : undefined,
666
);
667
668
function progress(
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+5
-1
@@ -239,7 +239,10 @@ function prerender(
239
function decodeReply<T>(
240
body: string | FormData,
241
webpackMap: ServerManifest,
242
- options?: {temporaryReferences?: TemporaryReferenceSet},
242
+ options?: {
243
+ temporaryReferences?: TemporaryReferenceSet,
244
+ arraySizeLimit?: number,
245
+ },
246
): Thenable<T> {
247
if (typeof body === 'string') {
248
const form = new FormData();
@@ -251,6 +254,7 @@ function decodeReply<T>(
254
'',
255
options ? options.temporaryReferences : undefined,
256
body,
257
+ options ? options.arraySizeLimit : undefined,
258
);
259
const root = getRoot<T>(response);
260
close(response);
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+11
-2
@@ -244,7 +244,10 @@ function prerender(
244
function decodeReply<T>(
245
body: string | FormData,
246
webpackMap: ServerManifest,
247
- options?: {temporaryReferences?: TemporaryReferenceSet},
247
+ options?: {
248
+ temporaryReferences?: TemporaryReferenceSet,
249
+ arraySizeLimit?: number,
250
+ },
251
): Thenable<T> {
252
if (typeof body === 'string') {
253
const form = new FormData();
@@ -256,6 +259,7 @@ function decodeReply<T>(
259
'',
260
options ? options.temporaryReferences : undefined,
261
body,
262
+ options ? options.arraySizeLimit : undefined,
263
);
264
const root = getRoot<T>(response);
265
close(response);
@@ -265,7 +269,10 @@ function decodeReply<T>(
269
function decodeReplyFromAsyncIterable<T>(
270
iterable: AsyncIterable<[string, string | File]>,
271
webpackMap: ServerManifest,
268
- options?: {temporaryReferences?: TemporaryReferenceSet},
272
+ options?: {
273
+ temporaryReferences?: TemporaryReferenceSet,
274
+ arraySizeLimit?: number,
275
+ },
276
): Thenable<T> {
277
const iterator: AsyncIterator<[string, string | File]> =
278
iterable[ASYNC_ITERATOR]();
@@ -274,6 +281,8 @@ function decodeReplyFromAsyncIterable<T>(
281
webpackMap,
282
'',
283
options ? options.temporaryReferences : undefined,
284
+ undefined,
285
+ options ? options.arraySizeLimit : undefined,
286
);
287
288
function progress(
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+17
-3
@@ -548,12 +548,17 @@ function prerender(
548
function decodeReplyFromBusboy<T>(
549
busboyStream: Busboy,
550
webpackMap: ServerManifest,
551
- options?: {temporaryReferences?: TemporaryReferenceSet},
551
+ options?: {
552
+ temporaryReferences?: TemporaryReferenceSet,
553
+ arraySizeLimit?: number,
554
+ },
555
): Thenable<T> {
556
const response = createResponse(
557
webpackMap,
558
'',
559
options ? options.temporaryReferences : undefined,
560
+ undefined,
561
+ options ? options.arraySizeLimit : undefined,
562
);
563
let pendingFiles = 0;
564
const queuedFields: Array<string> = [];
@@ -619,7 +624,10 @@ function decodeReplyFromBusboy<T>(
624
function decodeReply<T>(
625
body: string | FormData,
626
webpackMap: ServerManifest,
622
- options?: {temporaryReferences?: TemporaryReferenceSet},
627
+ options?: {
628
+ temporaryReferences?: TemporaryReferenceSet,
629
+ arraySizeLimit?: number,
630
+ },
631
): Thenable<T> {
632
if (typeof body === 'string') {
633
const form = new FormData();
@@ -631,6 +639,7 @@ function decodeReply<T>(
639
'',
640
options ? options.temporaryReferences : undefined,
641
body,
642
+ options ? options.arraySizeLimit : undefined,
643
);
644
const root = getRoot<T>(response);
645
close(response);
@@ -640,7 +649,10 @@ function decodeReply<T>(
649
function decodeReplyFromAsyncIterable<T>(
650
iterable: AsyncIterable<[string, string | File]>,
651
webpackMap: ServerManifest,
643
- options?: {temporaryReferences?: TemporaryReferenceSet},
652
+ options?: {
653
+ temporaryReferences?: TemporaryReferenceSet,
654
+ arraySizeLimit?: number,
655
+ },
656
): Thenable<T> {
657
const iterator: AsyncIterator<[string, string | File]> =
658
iterable[ASYNC_ITERATOR]();
@@ -649,6 +661,8 @@ function decodeReplyFromAsyncIterable<T>(
661
webpackMap,
662
'',
663
options ? options.temporaryReferences : undefined,
664
+ undefined,
665
+ options ? options.arraySizeLimit : undefined,
666
);
667
668
function progress(
packages/react-server/src/ReactFlightActionServer.js
+49
-9
@@ -7,7 +7,7 @@
7
* @flow
8
*/
9
10
-import type {Thenable, ReactFormState} from 'shared/ReactTypes';
10
+import type {ReactFormState} from 'shared/ReactTypes';
11
12
import type {
13
ServerManifest,
@@ -20,26 +20,48 @@ import {
20
requireModule,
21
} from 'react-client/src/ReactFlightClientConfig';
22
23
-import {createResponse, close, getRoot} from './ReactFlightReplyServer';
23
+import {
24
+ createResponse,
25
+ close,
26
+ getRoot,
27
+ MAX_BOUND_ARGS,
28
+} from './ReactFlightReplyServer';
29
30
type ServerReferenceId = any;
31
32
function bindArgs(fn: any, args: any) {
33
+ if (args.length > MAX_BOUND_ARGS) {
34
+ throw new Error(
35
+ 'Server Function has too many bound arguments. Received ' +
36
+ args.length +
37
+ ' but the limit is ' +
38
+ MAX_BOUND_ARGS +
39
+ '.',
40
+ );
41
+ }
42
+
43
return fn.bind.apply(fn, [null].concat(args));
44
}
45
46
function loadServerReference<T>(
47
bundlerConfig: ServerManifest,
33
- id: ServerReferenceId,
34
- bound: null | Thenable<Array<any>>,
48
+ metaData: {
49
+ id: string,
50
+ bound: null | Promise<Array<any>>,
51
+ },
52
): Promise<T> {
53
+ const id: ServerReferenceId = metaData.id;
54
+ if (typeof id !== 'string') {
55
+ return (null: any);
56
+ }
57
const serverReference: ServerReference<T> =
58
resolveServerReference<$FlowFixMe>(bundlerConfig, id);
59
// We expect most servers to not really need this because you'd just have all
60
// the relevant modules already loaded but it allows for lazy loading of code
61
// if needed.
62
const preloadPromise = preloadModule(serverReference);
42
- if (bound) {
63
+ const bound = metaData.bound;
64
+ if (bound instanceof Promise) {
65
return Promise.all([(bound: any), preloadPromise]).then(
66
([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
67
);
@@ -57,6 +79,7 @@ function decodeBoundActionMetaData(
79
body: FormData,
80
serverManifest: ServerManifest,
81
formFieldPrefix: string,
82
+ arraySizeLimit: void | number,
83
): {id: ServerReferenceId, bound: null | Promise<Array<any>>} {
84
// The data for this reference is encoded in multiple fields under this prefix.
85
const actionResponse = createResponse(
@@ -64,6 +87,7 @@ function decodeBoundActionMetaData(
87
formFieldPrefix,
88
undefined,
89
body,
90
+ arraySizeLimit,
91
);
92
close(actionResponse);
93
const refPromise = getRoot<{
@@ -89,6 +113,7 @@ export function decodeAction<T>(
113
const formData = new FormData();
114
115
let action: Promise<(formData: FormData) => T> | null = null;
116
+ const seenActions = new Set<string>();
117
118
// $FlowFixMe[prop-missing]
119
body.forEach((value: string | File, key: string) => {
@@ -97,21 +122,36 @@ export function decodeAction<T>(
122
formData.append(key, value);
123
return;
124
}
100
- // Later actions may override earlier actions if a button is used to override the default
101
- // form action.
125
+ // Later actions may override earlier actions if a button is used to
126
+ // override the default form action. However, we don't expect the same
127
+ // action ref field to be sent multiple times in legitimate form data.
128
if (key.startsWith('$ACTION_REF_')) {
129
+ if (seenActions.has(key)) {
130
+ return;
131
+ }
132
+ seenActions.add(key);
133
const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
134
const metaData = decodeBoundActionMetaData(
135
body,
136
serverManifest,
137
formFieldPrefix,
138
);
109
- action = loadServerReference(serverManifest, metaData.id, metaData.bound);
139
+ action = loadServerReference(serverManifest, metaData);
140
return;
141
}
142
+ // A simple action with no bound arguments may appear twice in the form data
143
+ // if a button specifies the same action as the default form action. We only
144
+ // load the first one, as they're guaranteed to be identical.
145
if (key.startsWith('$ACTION_ID_')) {
146
+ if (seenActions.has(key)) {
147
+ return;
148
+ }
149
+ seenActions.add(key);
150
const id = key.slice(11);
114
- action = loadServerReference(serverManifest, id, null);
151
+ action = loadServerReference(serverManifest, {
152
+ id,
153
+ bound: null,
154
+ });
155
return;
156
}
157
});
packages/react-server/src/ReactFlightReplyServer.js
+563
-196
@@ -34,6 +34,7 @@ import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
34
35
import hasOwnProperty from 'shared/hasOwnProperty';
36
import getPrototypeOf from 'shared/getPrototypeOf';
37
+import isArray from 'shared/isArray';
38
39
interface FlightStreamController {
40
enqueueModel(json: string): void;
@@ -55,6 +56,8 @@ const RESOLVED_MODEL = 'resolved_model';
56
const INITIALIZED = 'fulfilled';
57
const ERRORED = 'rejected';
58
59
+const __PROTO__ = '__proto__';
60
+
61
type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL'; // Fake symbol type.
62
const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = (Symbol(): any);
63
@@ -79,7 +82,7 @@ type ResolvedModelChunk<T> = {
82
type InitializedChunk<T> = {
83
status: 'fulfilled',
84
value: T,
82
- reason: null,
85
+ reason: null | NestedArrayContext,
86
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
87
};
88
type InitializedStreamChunk<
@@ -194,6 +197,8 @@ export type Response = {
197
_closed: boolean,
198
_closedReason: mixed,
199
_temporaryReferences: void | TemporaryReferenceSet,
200
+ _rootArrayContexts: WeakMap<$ReadOnlyArray<mixed>, NestedArrayContext>,
201
+ _arraySizeLimit: number,
202
};
203
204
export function getRoot<T>(response: Response): Thenable<T> {
@@ -210,13 +215,14 @@ function wakeChunk<T>(
215
response: Response,
216
listeners: Array<InitializationReference | (T => mixed)>,
217
value: T,
218
+ chunk: InitializedChunk<T>,
219
): void {
220
for (let i = 0; i < listeners.length; i++) {
221
const listener = listeners[i];
222
if (typeof listener === 'function') {
223
listener(value);
224
} else {
219
- fulfillReference(response, listener, value);
225
+ fulfillReference(response, listener, value, chunk.reason);
226
}
227
}
228
}
@@ -236,33 +242,6 @@ function rejectChunk(
242
}
243
}
244
239
-function resolveBlockedCycle<T>(
240
- resolvedChunk: SomeChunk<T>,
241
- reference: InitializationReference,
242
-): null | InitializationHandler {
243
- const referencedChunk = reference.handler.chunk;
244
- if (referencedChunk === null) {
245
- return null;
246
- }
247
- if (referencedChunk === resolvedChunk) {
248
- // We found the cycle. We can resolve the blocked cycle now.
249
- return reference.handler;
250
- }
251
- const resolveListeners = referencedChunk.value;
252
- if (resolveListeners !== null) {
253
- for (let i = 0; i < resolveListeners.length; i++) {
254
- const listener = resolveListeners[i];
255
- if (typeof listener !== 'function') {
256
- const foundHandler = resolveBlockedCycle(resolvedChunk, listener);
257
- if (foundHandler !== null) {
258
- return foundHandler;
259
- }
260
- }
261
- }
262
- }
263
- return null;
264
-}
265
-
245
function wakeChunkIfInitialized<T>(
246
response: Response,
247
chunk: SomeChunk<T>,
@@ -271,45 +250,9 @@ function wakeChunkIfInitialized<T>(
250
): void {
251
switch (chunk.status) {
252
case INITIALIZED:
274
- wakeChunk(response, resolveListeners, chunk.value);
253
+ wakeChunk(response, resolveListeners, chunk.value, chunk);
254
break;
255
case BLOCKED:
277
- // It is possible that we're blocked on our own chunk if it's a cycle.
278
- // Before adding back the listeners to the chunk, let's check if it would
279
- // result in a cycle.
280
- for (let i = 0; i < resolveListeners.length; i++) {
281
- const listener = resolveListeners[i];
282
- if (typeof listener !== 'function') {
283
- const reference: InitializationReference = listener;
284
- const cyclicHandler = resolveBlockedCycle(chunk, reference);
285
- if (cyclicHandler !== null) {
286
- // This reference points back to this chunk. We can resolve the cycle by
287
- // using the value from that handler.
288
- fulfillReference(response, reference, cyclicHandler.value);
289
- resolveListeners.splice(i, 1);
290
- i--;
291
- if (rejectListeners !== null) {
292
- const rejectionIdx = rejectListeners.indexOf(reference);
293
- if (rejectionIdx !== -1) {
294
- rejectListeners.splice(rejectionIdx, 1);
295
- }
296
- }
297
- // The status might have changed after fulfilling the reference.
298
- switch ((chunk: SomeChunk<T>).status) {
299
- case INITIALIZED:
300
- const initializedChunk: InitializedChunk<T> = (chunk: any);
301
- wakeChunk(response, resolveListeners, initializedChunk.value);
302
- return;
303
- case ERRORED:
304
- if (rejectListeners !== null) {
305
- rejectChunk(response, rejectListeners, chunk.reason);
306
- }
307
- return;
308
- }
309
- }
310
- }
311
- }
312
- // Fallthrough
256
case PENDING:
257
if (chunk.value) {
258
for (let i = 0; i < resolveListeners.length; i++) {
@@ -331,7 +274,7 @@ function wakeChunkIfInitialized<T>(
274
break;
275
case ERRORED:
276
if (rejectListeners) {
334
- wakeChunk(response, rejectListeners, chunk.reason);
277
+ rejectChunk(response, rejectListeners, chunk.reason);
278
}
279
break;
280
}
@@ -472,22 +415,73 @@ function loadServerReference<A: Iterable<any>, T>(
415
// as "thenable" which reduces to ReactPromise with no other fields.
416
return (null: any);
417
}
418
+
419
+ // Check for a cached promise from a previous call with the same metadata.
420
+ // This handles deduplication when the same server reference appears multiple
421
+ // times in the payload.
422
+ const cachedPromise: SomeChunk<T> | void = (metaData: any).$$promise;
423
+ if (cachedPromise !== undefined) {
424
+ if (cachedPromise.status === INITIALIZED) {
425
+ // The value was already resolved by a previous call.
426
+ const resolvedValue: T = cachedPromise.value;
427
+ if (key === __PROTO__) {
428
+ return (null: any);
429
+ }
430
+ parentObject[key] = resolvedValue;
431
+ return (resolvedValue: any);
432
+ }
433
+
434
+ // The promise is still blocked. Increment the handler dependency count ...
435
+ let handler: InitializationHandler;
436
+ if (initializingHandler) {
437
+ handler = initializingHandler;
438
+ handler.deps++;
439
+ } else {
440
+ handler = initializingHandler = {
441
+ chunk: null,
442
+ value: null,
443
+ reason: null,
444
+ deps: 1,
445
+ errored: false,
446
+ };
447
+ }
448
+ // ... and register resolve and reject listeners on the promise.
449
+ cachedPromise.then(
450
+ resolveReference.bind(null, response, handler, parentObject, key),
451
+ rejectReference.bind(null, response, handler),
452
+ );
453
+
454
+ // Return a place holder value for now.
455
+ return (null: any);
456
+ }
457
+
458
+ // This is the first call for this server reference metadata. Create a cached
459
+ // promise to be used for subsequent calls.
460
+ // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
461
+ const blockedPromise: BlockedChunk<T> = new ReactPromise(BLOCKED, null, null);
462
+ (metaData: any).$$promise = blockedPromise;
463
+
464
const serverReference: ServerReference<T> =
465
resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
466
// We expect most servers to not really need this because you'd just have all
467
// the relevant modules already loaded but it allows for lazy loading of code
468
// if needed.
469
const bound = metaData.bound;
481
- let promise: null | Thenable<any> = preloadModule(serverReference);
482
- if (!promise) {
470
+ let serverReferencePromise: null | Thenable<any> =
471
+ preloadModule(serverReference);
472
+ if (!serverReferencePromise) {
473
if (bound instanceof ReactPromise) {
484
- promise = Promise.resolve(bound);
474
+ serverReferencePromise = Promise.resolve(bound);
475
} else {
476
const resolvedValue = (requireModule(serverReference): any);
477
+ // Resolve the cached promise synchronously.
478
+ const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
479
+ initializedPromise.status = INITIALIZED;
480
+ initializedPromise.value = resolvedValue;
481
return resolvedValue;
482
}
483
} else if (bound instanceof ReactPromise) {
490
- promise = Promise.all([promise, bound]);
484
+ serverReferencePromise = Promise.all([serverReferencePromise, bound]);
485
}
486
487
let handler: InitializationHandler;
@@ -508,59 +502,59 @@ function loadServerReference<A: Iterable<any>, T>(
502
let resolvedValue = (requireModule(serverReference): any);
503
504
if (metaData.bound) {
511
- // This promise is coming from us and should have initilialized by now.
505
+ // This promise is coming from us and should have initialized by now.
506
const promiseValue = (metaData.bound: any).value;
513
- const boundArgs: Array<any> = Array.isArray(promiseValue)
507
+ const boundArgs: Array<any> = isArray(promiseValue)
508
? promiseValue.slice(0)
509
: [];
510
+ if (boundArgs.length > MAX_BOUND_ARGS) {
511
+ reject(
512
+ new Error(
513
+ 'Server Function has too many bound arguments. Received ' +
514
+ boundArgs.length +
515
+ ' but the limit is ' +
516
+ MAX_BOUND_ARGS +
517
+ '.',
518
+ ),
519
+ );
520
+ return;
521
+ }
522
boundArgs.unshift(null); // this
523
resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
524
}
525
520
- parentObject[key] = resolvedValue;
521
-
522
- // If this is the root object for a model reference, where `handler.value`
523
- // is a stale `null`, the resolved value can be used directly.
524
- if (key === '' && handler.value === null) {
525
- handler.value = resolvedValue;
526
+ // Resolve the cached promise so subsequent references can use the value.
527
+ const resolveListeners = blockedPromise.value;
528
+ const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
529
+ initializedPromise.status = INITIALIZED;
530
+ initializedPromise.value = resolvedValue;
531
+ initializedPromise.reason = null;
532
+ if (resolveListeners !== null) {
533
+ // Notify any resolve listeners that were added via .then() from
534
+ // subsequent loadServerReference calls for the same reference.
535
+ wakeChunk(response, resolveListeners, resolvedValue, initializedPromise);
536
}
537
528
- handler.deps--;
529
-
530
- if (handler.deps === 0) {
531
- const chunk = handler.chunk;
532
- if (chunk === null || chunk.status !== BLOCKED) {
533
- return;
534
- }
535
- const resolveListeners = chunk.value;
536
- const initializedChunk: InitializedChunk<T> = (chunk: any);
537
- initializedChunk.status = INITIALIZED;
538
- initializedChunk.value = handler.value;
539
- initializedChunk.reason = null;
540
- if (resolveListeners !== null) {
541
- wakeChunk(response, resolveListeners, handler.value);
542
- }
543
- }
538
+ resolveReference(response, handler, parentObject, key, resolvedValue);
539
}
540
541
function reject(error: mixed): void {
547
- if (handler.errored) {
548
- // We've already errored. We could instead build up an AggregateError
549
- // but if there are multiple errors we just take the first one like
550
- // Promise.all.
551
- return;
552
- }
553
- handler.errored = true;
554
- handler.value = null;
555
- handler.reason = error;
556
- const chunk = handler.chunk;
557
- if (chunk === null || chunk.status !== BLOCKED) {
558
- return;
542
+ // Mark the cached promise as errored so subsequent references fail too.
543
+ const rejectListeners = blockedPromise.reason;
544
+ const erroredPromise: ErroredChunk<T> = (blockedPromise: any);
545
+ erroredPromise.status = ERRORED;
546
+ erroredPromise.value = null;
547
+ erroredPromise.reason = error;
548
+ if (rejectListeners !== null) {
549
+ // Notify any reject listeners that were added via .then() from subsequent
550
+ // loadServerReference calls for the same reference.
551
+ rejectChunk(response, rejectListeners, error);
552
}
560
- triggerErrorOnChunk(response, chunk, error);
553
+
554
+ rejectReference(response, handler, error);
555
}
556
563
- promise.then(fulfill, reject);
557
+ serverReferencePromise.then(fulfill, reject);
558
559
// Return a place holder value for now.
560
return (null: any);
@@ -572,10 +566,18 @@ function reviveModel(
566
parentKey: string,
567
value: JSONValue,
568
reference: void | string,
569
+ arrayRoot: null | NestedArrayContext,
570
): any {
571
if (typeof value === 'string') {
572
// We can't use .bind here because we need the "this" value.
578
- return parseModelString(response, parentObj, parentKey, value, reference);
573
+ return parseModelString(
574
+ response,
575
+ parentObj,
576
+ parentKey,
577
+ value,
578
+ reference,
579
+ arrayRoot,
580
+ );
581
}
582
if (typeof value === 'object' && value !== null) {
583
if (
@@ -589,16 +591,42 @@ function reviveModel(
591
reference,
592
);
593
}
592
- if (Array.isArray(value)) {
594
+ if (isArray(value)) {
595
+ let childContext: NestedArrayContext;
596
+ if (arrayRoot === null) {
597
+ childContext = ({
598
+ count: 0,
599
+ fork: false,
600
+ }: NestedArrayContext);
601
+ response._rootArrayContexts.set(value, childContext);
602
+ } else {
603
+ childContext = arrayRoot;
604
+ }
605
+ if (value.length > 1) {
606
+ childContext.fork = true;
607
+ }
608
+ bumpArrayCount(childContext, value.length + 1, response);
609
for (let i = 0; i < value.length; i++) {
610
const childRef =
611
reference !== undefined ? reference + ':' + i : undefined;
612
// $FlowFixMe[cannot-write]
597
- value[i] = reviveModel(response, value, '' + i, value[i], childRef);
613
+ value[i] = reviveModel(
614
+ response,
615
+ value,
616
+ '' + i,
617
+ value[i],
618
+ childRef,
619
+ childContext,
620
+ );
621
}
622
} else {
623
for (const key in value) {
624
if (hasOwnProperty.call(value, key)) {
625
+ if (key === __PROTO__) {
626
+ // $FlowFixMe[cannot-write]
627
+ delete value[key];
628
+ continue;
629
+ }
630
const childRef =
631
reference !== undefined && key.indexOf(':') === -1
632
? reference + ':' + key
@@ -609,8 +637,9 @@ function reviveModel(
637
key,
638
value[key],
639
childRef,
640
+ null, // The array context resets when we're entering a non-array
641
);
613
- if (newValue !== undefined || key === '__proto__') {
642
+ if (newValue !== undefined) {
643
// $FlowFixMe[cannot-write]
644
value[key] = newValue;
645
} else {
@@ -624,6 +653,27 @@ function reviveModel(
653
return value;
654
}
655
656
+type NestedArrayContext = {
657
+ // Keeps track of how many slots, bytes or characters are in nested arrays/strings/typed arrays.
658
+ count: number,
659
+ // A single child is itself not harmful. There needs to be at least one parent array with more
660
+ // than one child.
661
+ fork: boolean,
662
+};
663
+
664
+function bumpArrayCount(
665
+ arrayContext: NestedArrayContext,
666
+ slots: number,
667
+ response: Response,
668
+): void {
669
+ const newCount = (arrayContext.count += slots);
670
+ if (newCount > response._arraySizeLimit && arrayContext.fork) {
671
+ throw new Error(
672
+ 'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.',
673
+ );
674
+ }
675
+}
676
+
677
type InitializationReference = {
678
handler: InitializationHandler,
679
parentObject: Object,
@@ -635,6 +685,7 @@ type InitializationReference = {
685
key: string,
686
) => any,
687
path: Array<string>,
688
+ arrayRoot: null | NestedArrayContext,
689
};
690
type InitializationHandler = {
691
chunk: null | BlockedChunk<any>,
@@ -666,12 +717,19 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
717
try {
718
const rawModel = JSON.parse(resolvedModel);
719
720
+ // The root might not be an array but if it is we want to track the count of entries.
721
+ const arrayRoot: NestedArrayContext = {
722
+ count: 0,
723
+ fork: false,
724
+ };
725
+
726
const value: T = reviveModel(
727
response,
728
{'': rawModel},
729
'',
730
rawModel,
731
rootReference,
732
+ arrayRoot,
733
);
734
735
// Invoke any listeners added while resolving this model. I.e. cyclic
@@ -686,7 +744,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
744
if (typeof listener === 'function') {
745
listener(value);
746
} else {
689
- fulfillReference(response, listener, value);
747
+ fulfillReference(response, listener, value, arrayRoot);
748
}
749
}
750
}
@@ -698,6 +756,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
756
// We discovered new dependencies on modules that are not yet resolved.
757
// We have to keep the BLOCKED state until they're resolved.
758
initializingHandler.value = value;
759
+ initializingHandler.reason = arrayRoot;
760
initializingHandler.chunk = cyclicChunk;
761
return;
762
}
@@ -705,7 +764,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
764
const initializedChunk: InitializedChunk<T> = (chunk: any);
765
initializedChunk.status = INITIALIZED;
766
initializedChunk.value = value;
708
- initializedChunk.reason = null;
767
+ initializedChunk.reason = arrayRoot;
768
} catch (error) {
769
const erroredChunk: ErroredChunk<T> = (chunk: any);
770
erroredChunk.status = ERRORED;
@@ -727,7 +786,11 @@ export function reportGlobalError(response: Response, error: Error): void {
786
if (chunk.status === PENDING) {
787
triggerErrorOnChunk(response, chunk, error);
788
} else if (chunk.status === INITIALIZED && chunk.reason !== null) {
730
- chunk.reason.error(error);
789
+ const maybeController = chunk.reason;
790
+ // $FlowFixMe
791
+ if (typeof maybeController.error === 'function') {
792
+ maybeController.error(error);
793
+ }
794
}
795
});
796
}
@@ -759,10 +822,14 @@ function fulfillReference(
822
response: Response,
823
reference: InitializationReference,
824
value: any,
825
+ arrayRoot: null | NestedArrayContext,
826
): void {
827
const {handler, parentObject, key, map, path} = reference;
828
829
+ let resolvedValue;
830
try {
831
+ let localLength: number = 0;
832
+ const rootArrayContexts = response._rootArrayContexts;
833
for (let i = 1; i < path.length; i++) {
834
// The server doesn't have any lazy references so we don't expect to go through a Promise.
835
const name = path[i];
@@ -774,26 +841,77 @@ function fulfillReference(
841
hasOwnProperty.call(value, name)
842
) {
843
value = value[name];
844
+ if (isArray(value)) {
845
+ localLength = 0;
846
+ arrayRoot = rootArrayContexts.get(value) || arrayRoot;
847
+ } else {
848
+ arrayRoot = null;
849
+ if (typeof value === 'string') {
850
+ localLength = value.length;
851
+ } else if (typeof value === 'bigint') {
852
+ // Estimate the length to avoid expensive toString() calls on large
853
+ // BigInt values. If the value is too large, we get Infinity, which
854
+ // will trigger the array size limit error.
855
+ // eslint-disable-next-line react-internal/no-primitive-constructors
856
+ const n = Math.abs(Number(value));
857
+ if (n === 0) {
858
+ localLength = 1;
859
+ } else {
860
+ localLength = Math.floor(Math.log10(n)) + 1;
861
+ }
862
+ } else if (ArrayBuffer.isView(value)) {
863
+ localLength = value.byteLength;
864
+ } else {
865
+ localLength = 0;
866
+ }
867
+ }
868
} else {
869
throw new Error('Invalid reference.');
870
}
871
}
872
782
- const mappedValue = map(response, value, parentObject, key);
783
- parentObject[key] = mappedValue;
873
+ resolvedValue = map(response, value, parentObject, key);
874
785
- // If this is the root object for a model reference, where `handler.value`
786
- // is a stale `null`, the resolved value can be used directly.
787
- if (key === '' && handler.value === null) {
788
- handler.value = mappedValue;
875
+ // Add any array counts to the reference's array root. The value that we're
876
+ // resolving might have deep nesting that we need to resolve.
877
+ const referenceArrayRoot = reference.arrayRoot;
878
+ if (referenceArrayRoot !== null) {
879
+ if (arrayRoot !== null) {
880
+ if (arrayRoot.fork) {
881
+ referenceArrayRoot.fork = true;
882
+ }
883
+ bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
884
+ } else if (localLength > 0) {
885
+ bumpArrayCount(referenceArrayRoot, localLength, response);
886
+ }
887
}
888
} catch (error) {
791
- rejectReference(response, reference.handler, error);
889
+ rejectReference(response, handler, error);
890
return;
891
}
892
893
// There are no Elements or Debug Info to transfer here.
894
895
+ resolveReference(response, handler, parentObject, key, resolvedValue);
896
+}
897
+
898
+function resolveReference(
899
+ response: Response,
900
+ handler: InitializationHandler,
901
+ parentObject: Object,
902
+ key: string,
903
+ resolvedValue: mixed,
904
+): void {
905
+ if (key !== __PROTO__) {
906
+ parentObject[key] = resolvedValue;
907
+ }
908
+
909
+ // If this is the root object for a model reference, where `handler.value`
910
+ // is a stale `null`, the resolved value can be used directly.
911
+ if (key === '' && handler.value === null) {
912
+ handler.value = resolvedValue;
913
+ }
914
+
915
handler.deps--;
916
917
if (handler.deps === 0) {
@@ -807,7 +925,7 @@ function fulfillReference(
925
initializedChunk.value = handler.value;
926
initializedChunk.reason = handler.reason; // Used by streaming chunks
927
if (resolveListeners !== null) {
810
- wakeChunk(response, resolveListeners, handler.value);
928
+ wakeChunk(response, resolveListeners, handler.value, initializedChunk);
929
}
930
}
931
}
@@ -835,10 +953,11 @@ function rejectReference(
953
}
954
955
function waitForReference<T>(
838
- referencedChunk: PendingChunk<T> | BlockedChunk<T>,
956
+ response: Response,
957
+ referencedChunk: BlockedChunk<T>,
958
parentObject: Object,
959
key: string,
841
- response: Response,
960
+ arrayRoot: null | NestedArrayContext,
961
map: (response: Response, model: any, parentObject: Object, key: string) => T,
962
path: Array<string>,
963
): T {
@@ -862,6 +981,7 @@ function waitForReference<T>(
981
key,
982
map,
983
path,
984
+ arrayRoot,
985
};
986
987
// Add "listener".
@@ -885,6 +1005,7 @@ function getOutlinedModel<T>(
1005
reference: string,
1006
parentObject: Object,
1007
key: string,
1008
+ referenceArrayRoot: null | NestedArrayContext,
1009
map: (response: Response, model: any, parentObject: Object, key: string) => T,
1010
): T {
1011
const path = reference.split(':');
@@ -899,6 +1020,9 @@ function getOutlinedModel<T>(
1020
switch (chunk.status) {
1021
case INITIALIZED:
1022
let value = chunk.value;
1023
+ let arrayRoot: null | NestedArrayContext = chunk.reason;
1024
+ let localLength: number = 0;
1025
+ const rootArrayContexts = response._rootArrayContexts;
1026
for (let i = 1; i < path.length; i++) {
1027
const name = path[i];
1028
if (
@@ -909,16 +1033,64 @@ function getOutlinedModel<T>(
1033
hasOwnProperty.call(value, name)
1034
) {
1035
value = value[name];
1036
+ if (isArray(value)) {
1037
+ localLength = 0;
1038
+ arrayRoot = rootArrayContexts.get(value) || arrayRoot;
1039
+ } else {
1040
+ arrayRoot = null;
1041
+ if (typeof value === 'string') {
1042
+ localLength = value.length;
1043
+ } else if (typeof value === 'bigint') {
1044
+ // Estimate the length to avoid expensive toString() calls on large
1045
+ // BigInt values. If the value is too large, we get Infinity, which
1046
+ // will trigger the array size limit error.
1047
+ // eslint-disable-next-line react-internal/no-primitive-constructors
1048
+ const n = Math.abs(Number(value));
1049
+ if (n === 0) {
1050
+ localLength = 1;
1051
+ } else {
1052
+ localLength = Math.floor(Math.log10(n)) + 1;
1053
+ }
1054
+ } else if (ArrayBuffer.isView(value)) {
1055
+ localLength = value.byteLength;
1056
+ } else {
1057
+ localLength = 0;
1058
+ }
1059
+ }
1060
} else {
1061
throw new Error('Invalid reference.');
1062
}
1063
}
1064
const chunkValue = map(response, value, parentObject, key);
1065
+
1066
+ // Add any array counts to the reference's array root. The value that we're
1067
+ // resolving might have deep nesting that we need to resolve.
1068
+ if (referenceArrayRoot !== null) {
1069
+ if (arrayRoot !== null) {
1070
+ if (arrayRoot.fork) {
1071
+ referenceArrayRoot.fork = true;
1072
+ }
1073
+ bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
1074
+ } else if (localLength > 0) {
1075
+ bumpArrayCount(referenceArrayRoot, localLength, response);
1076
+ }
1077
+ }
1078
// There's no Element nor Debug Info in the ReplyServer so we don't have to check those here.
1079
return chunkValue;
919
- case PENDING:
1080
case BLOCKED:
921
- return waitForReference(chunk, parentObject, key, response, map, path);
1081
+ return waitForReference(
1082
+ response,
1083
+ chunk,
1084
+ parentObject,
1085
+ key,
1086
+ referenceArrayRoot,
1087
+ map,
1088
+ path,
1089
+ );
1090
+ case PENDING:
1091
+ // If we don't have the referenced chunk yet, then this must be a forward reference,
1092
+ // which is not allowed.
1093
+ throw new Error('Invalid forward reference.');
1094
default:
1095
// This is an error. Instead of erroring directly, we're going to encode this on
1096
// an initialization handler.
@@ -944,16 +1116,40 @@ function createMap(
1116
response: Response,
1117
model: Array<[any, any]>,
1118
): Map<any, any> {
947
- return new Map(model);
1119
+ if (!isArray(model)) {
1120
+ throw new Error('Invalid Map initializer.');
1121
+ }
1122
+ if ((model as any).$$consumed === true) {
1123
+ throw new Error('Already initialized Map.');
1124
+ }
1125
+ const map = new Map(model);
1126
+ (model as any).$$consumed = true;
1127
+ return map;
1128
}
1129
1130
function createSet(response: Response, model: Array<any>): Set<any> {
951
- return new Set(model);
1131
+ if (!isArray(model)) {
1132
+ throw new Error('Invalid Set initializer.');
1133
+ }
1134
+ if ((model as any).$$consumed === true) {
1135
+ throw new Error('Already initialized Set.');
1136
+ }
1137
+ const set = new Set(model);
1138
+ (model as any).$$consumed = true;
1139
+ return set;
1140
}
1141
1142
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
1143
+ if (!isArray(model)) {
1144
+ throw new Error('Invalid Iterator initializer.');
1145
+ }
1146
+ if ((model as any).$$consumed === true) {
1147
+ throw new Error('Already initialized Iterator.');
1148
+ }
1149
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
956
- return model[Symbol.iterator]();
1150
+ const iterator = model[Symbol.iterator]();
1151
+ (model as any).$$consumed = true;
1152
+ return iterator;
1153
}
1154
1155
function createModel(
@@ -977,6 +1173,7 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
1173
bytesPerElement: number,
1174
parentObject: Object,
1175
parentKey: string,
1176
+ referenceArrayRoot: null | NestedArrayContext,
1177
): null {
1178
const id = parseInt(reference.slice(2), 16);
1179
const prefix = response._prefix;
@@ -985,10 +1182,15 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
1182
if (chunks.has(id)) {
1183
throw new Error('Already initialized typed array.');
1184
}
1185
+ chunks.set(
1186
+ id,
1187
+ // We don't need to put the actual Blob in the chunk,
1188
+ // because it shouldn't be accessed by anything else.
1189
+ createErroredChunk(response, new Error('Already initialized typed array.')),
1190
+ );
1191
1192
// We should have this backingEntry in the store already because we emitted
1193
// it before referencing it. It should be a Blob.
991
- // TODO: Use getOutlinedModel to allow us to emit the Blob later. We should be able to do that now.
1194
const backingEntry: Blob = (response._formData.get(key): any);
1195
1196
const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();
@@ -1011,17 +1213,28 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
1213
}
1214
1215
function fulfill(buffer: ArrayBuffer): void {
1014
- const resolvedValue: T =
1015
- constructor === ArrayBuffer
1016
- ? (buffer: any)
1017
- : (new constructor(buffer): any);
1216
+ try {
1217
+ if (referenceArrayRoot !== null) {
1218
+ bumpArrayCount(referenceArrayRoot, buffer.byteLength, response);
1219
+ }
1220
1019
- parentObject[parentKey] = resolvedValue;
1221
+ const resolvedValue: T =
1222
+ constructor === ArrayBuffer
1223
+ ? (buffer: any)
1224
+ : (new constructor(buffer): any);
1225
1021
- // If this is the root object for a model reference, where `handler.value`
1022
- // is a stale `null`, the resolved value can be used directly.
1023
- if (parentKey === '' && handler.value === null) {
1024
- handler.value = resolvedValue;
1226
+ if (key !== __PROTO__) {
1227
+ parentObject[parentKey] = resolvedValue;
1228
+ }
1229
+
1230
+ // If this is the root object for a model reference, where `handler.value`
1231
+ // is a stale `null`, the resolved value can be used directly.
1232
+ if (parentKey === '' && handler.value === null) {
1233
+ handler.value = resolvedValue;
1234
+ }
1235
+ } catch (x) {
1236
+ reject(x);
1237
+ return;
1238
}
1239
1240
handler.deps--;
@@ -1035,9 +1248,11 @@ function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
1248
const initializedChunk: InitializedChunk<T> = (chunk: any);
1249
initializedChunk.status = INITIALIZED;
1250
initializedChunk.value = handler.value;
1251
+ // We don't keep an array count for this since it won't be referenced again.
1252
+ // In fact, we don't really need to store this chunk at all.
1253
initializedChunk.reason = null;
1254
if (resolveListeners !== null) {
1040
- wakeChunk(response, resolveListeners, handler.value);
1255
+ wakeChunk(response, resolveListeners, handler.value, initializedChunk);
1256
}
1257
}
1258
}
@@ -1111,6 +1326,13 @@ function parseReadableStream<T>(
1326
},
1327
});
1328
let previousBlockedChunk: SomeChunk<T> | null = null;
1329
+ function enqueue(value: T): void {
1330
+ if (type === 'bytes' && !ArrayBuffer.isView(value)) {
1331
+ flightController.error(new Error('Invalid data for bytes stream.'));
1332
+ return;
1333
+ }
1334
+ controller.enqueue(value);
1335
+ }
1336
const flightController = {
1337
enqueueModel(json: string): void {
1338
if (previousBlockedChunk === null) {
@@ -1124,22 +1346,16 @@ function parseReadableStream<T>(
1346
initializeModelChunk(chunk);
1347
const initializedChunk: SomeChunk<T> = chunk;
1348
if (initializedChunk.status === INITIALIZED) {
1127
- controller.enqueue(initializedChunk.value);
1349
+ enqueue(initializedChunk.value);
1350
} else {
1129
- chunk.then(
1130
- v => controller.enqueue(v),
1131
- e => controller.error((e: any)),
1132
- );
1351
+ chunk.then(enqueue, flightController.error);
1352
previousBlockedChunk = chunk;
1353
}
1354
} else {
1355
// We're still waiting on a previous chunk so we can't enqueue quite yet.
1356
const blockedChunk = previousBlockedChunk;
1357
const chunk: SomeChunk<T> = createPendingChunk(response);
1139
- chunk.then(
1140
- v => controller.enqueue(v),
1141
- e => controller.error((e: any)),
1142
- );
1358
+ chunk.then(enqueue, flightController.error);
1359
previousBlockedChunk = chunk;
1360
blockedChunk.then(function () {
1361
if (previousBlockedChunk === chunk) {
@@ -1185,24 +1401,23 @@ function parseReadableStream<T>(
1401
return stream;
1402
}
1403
1188
-function asyncIterator(this: $AsyncIterator<any, any, void>) {
1404
+function FlightIterator(
1405
+ this: {next: (arg: void) => SomeChunk<IteratorResult<any, any>>, ...},
1406
+ next: (arg: void) => SomeChunk<IteratorResult<any, any>>,
1407
+) {
1408
+ this.next = next;
1409
+ // TODO: Add return/throw as options for aborting.
1410
+}
1411
+// TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as
1412
+// a global but exists as a prototype of an AsyncGenerator. However, it's not needed
1413
+// to satisfy the iterable protocol.
1414
+FlightIterator.prototype = ({}: any);
1415
+FlightIterator.prototype[ASYNC_ITERATOR] = function asyncIterator(
1416
+ this: $AsyncIterator<any, any, void>,
1417
+) {
1418
// Self referencing iterator.
1419
return this;
1191
-}
1192
-
1193
-function createIterator<T>(
1194
- next: (arg: void) => SomeChunk<IteratorResult<T, T>>,
1195
-): $AsyncIterator<T, T, void> {
1196
- const iterator: any = {
1197
- next: next,
1198
- // TODO: Add return/throw as options for aborting.
1199
- };
1200
- // TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as
1201
- // a global but exists as a prototype of an AsyncGenerator. However, it's not needed
1202
- // to satisfy the iterable protocol.
1203
- (iterator: any)[ASYNC_ITERATOR] = asyncIterator;
1204
- return iterator;
1205
-}
1420
+};
1421
1422
function parseAsyncIterable<T>(
1423
response: Response,
@@ -1285,7 +1500,8 @@ function parseAsyncIterable<T>(
1500
const iterable: $AsyncIterable<T, T, void> = {
1501
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
1502
let nextReadIndex = 0;
1288
- return createIterator(arg => {
1503
+ // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
1504
+ return new FlightIterator((arg: void) => {
1505
if (arg !== undefined) {
1506
throw new Error(
1507
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
@@ -1320,11 +1536,15 @@ function parseModelString(
1536
key: string,
1537
value: string,
1538
reference: void | string,
1539
+ arrayRoot: null | NestedArrayContext,
1540
): any {
1541
if (value[0] === '$') {
1542
switch (value[1]) {
1543
case '$': {
1544
// This was an escaped string value.
1545
+ if (arrayRoot !== null) {
1546
+ bumpArrayCount(arrayRoot, value.length - 1, response);
1547
+ }
1548
return value.slice(1);
1549
}
1550
case '@': {
@@ -1336,7 +1556,14 @@ function parseModelString(
1556
case 'h': {
1557
// Server Reference
1558
const ref = value.slice(2);
1339
- return getOutlinedModel(response, ref, obj, key, loadServerReference);
1559
+ return getOutlinedModel(
1560
+ response,
1561
+ ref,
1562
+ obj,
1563
+ key,
1564
+ null,
1565
+ loadServerReference,
1566
+ );
1567
}
1568
case 'T': {
1569
// Temporary Reference
@@ -1358,12 +1585,12 @@ function parseModelString(
1585
case 'Q': {
1586
// Map
1587
const ref = value.slice(2);
1361
- return getOutlinedModel(response, ref, obj, key, createMap);
1588
+ return getOutlinedModel(response, ref, obj, key, null, createMap);
1589
}
1590
case 'W': {
1591
// Set
1592
const ref = value.slice(2);
1366
- return getOutlinedModel(response, ref, obj, key, createSet);
1593
+ return getOutlinedModel(response, ref, obj, key, null, createSet);
1594
}
1595
case 'K': {
1596
// FormData
@@ -1374,19 +1601,30 @@ function parseModelString(
1601
// We assume that the reference to FormData always comes after each
1602
// entry that it references so we can assume they all exist in the
1603
// backing store already.
1377
- // $FlowFixMe[prop-missing] FormData has forEach on it.
1378
- backingFormData.forEach((entry: File | string, entryKey: string) => {
1604
+ // Clone the keys to workaround bugs in the delete-while-iterating
1605
+ // algorithm of FormData.
1606
+ const keys = Array.from(backingFormData.keys());
1607
+ for (let i = 0; i < keys.length; i++) {
1608
+ const entryKey = keys[i];
1609
if (entryKey.startsWith(formPrefix)) {
1380
- // $FlowFixMe[incompatible-call]
1381
- data.append(entryKey.slice(formPrefix.length), entry);
1610
+ const entries = backingFormData.getAll(entryKey);
1611
+ const newKey = entryKey.slice(formPrefix.length);
1612
+ for (let j = 0; j < entries.length; j++) {
1613
+ // $FlowFixMe[incompatible-call]
1614
+ data.append(newKey, entries[j]);
1615
+ }
1616
+ // These entries have now all been consumed. Let's free it.
1617
+ // This also ensures that we don't have any entries left if we
1618
+ // see the same key twice.
1619
+ backingFormData.delete(entryKey);
1620
}
1383
- });
1621
+ }
1622
return data;
1623
}
1624
case 'i': {
1625
// Iterator
1626
const ref = value.slice(2);
1389
- return getOutlinedModel(response, ref, obj, key, extractIterator);
1627
+ return getOutlinedModel(response, ref, obj, key, null, extractIterator);
1628
}
1629
case 'I': {
1630
// $Infinity
@@ -1415,36 +1653,151 @@ function parseModelString(
1653
}
1654
case 'n': {
1655
// BigInt
1418
- return BigInt(value.slice(2));
1656
+ const bigIntStr = value.slice(2);
1657
+ if (bigIntStr.length > MAX_BIGINT_DIGITS) {
1658
+ throw new Error(
1659
+ 'BigInt is too large. Received ' +
1660
+ bigIntStr.length +
1661
+ ' digits but the limit is ' +
1662
+ MAX_BIGINT_DIGITS +
1663
+ '.',
1664
+ );
1665
+ }
1666
+ if (arrayRoot !== null) {
1667
+ bumpArrayCount(arrayRoot, bigIntStr.length, response);
1668
+ }
1669
+ return BigInt(bigIntStr);
1670
}
1420
- }
1421
- switch (value[1]) {
1671
case 'A':
1423
- return parseTypedArray(response, value, ArrayBuffer, 1, obj, key);
1672
+ return parseTypedArray(
1673
+ response,
1674
+ value,
1675
+ ArrayBuffer,
1676
+ 1,
1677
+ obj,
1678
+ key,
1679
+ arrayRoot,
1680
+ );
1681
case 'O':
1425
- return parseTypedArray(response, value, Int8Array, 1, obj, key);
1682
+ return parseTypedArray(
1683
+ response,
1684
+ value,
1685
+ Int8Array,
1686
+ 1,
1687
+ obj,
1688
+ key,
1689
+ arrayRoot,
1690
+ );
1691
case 'o':
1427
- return parseTypedArray(response, value, Uint8Array, 1, obj, key);
1692
+ return parseTypedArray(
1693
+ response,
1694
+ value,
1695
+ Uint8Array,
1696
+ 1,
1697
+ obj,
1698
+ key,
1699
+ arrayRoot,
1700
+ );
1701
case 'U':
1429
- return parseTypedArray(response, value, Uint8ClampedArray, 1, obj, key);
1702
+ return parseTypedArray(
1703
+ response,
1704
+ value,
1705
+ Uint8ClampedArray,
1706
+ 1,
1707
+ obj,
1708
+ key,
1709
+ arrayRoot,
1710
+ );
1711
case 'S':
1431
- return parseTypedArray(response, value, Int16Array, 2, obj, key);
1712
+ return parseTypedArray(
1713
+ response,
1714
+ value,
1715
+ Int16Array,
1716
+ 2,
1717
+ obj,
1718
+ key,
1719
+ arrayRoot,
1720
+ );
1721
case 's':
1433
- return parseTypedArray(response, value, Uint16Array, 2, obj, key);
1722
+ return parseTypedArray(
1723
+ response,
1724
+ value,
1725
+ Uint16Array,
1726
+ 2,
1727
+ obj,
1728
+ key,
1729
+ arrayRoot,
1730
+ );
1731
case 'L':
1435
- return parseTypedArray(response, value, Int32Array, 4, obj, key);
1732
+ return parseTypedArray(
1733
+ response,
1734
+ value,
1735
+ Int32Array,
1736
+ 4,
1737
+ obj,
1738
+ key,
1739
+ arrayRoot,
1740
+ );
1741
case 'l':
1437
- return parseTypedArray(response, value, Uint32Array, 4, obj, key);
1742
+ return parseTypedArray(
1743
+ response,
1744
+ value,
1745
+ Uint32Array,
1746
+ 4,
1747
+ obj,
1748
+ key,
1749
+ arrayRoot,
1750
+ );
1751
case 'G':
1439
- return parseTypedArray(response, value, Float32Array, 4, obj, key);
1752
+ return parseTypedArray(
1753
+ response,
1754
+ value,
1755
+ Float32Array,
1756
+ 4,
1757
+ obj,
1758
+ key,
1759
+ arrayRoot,
1760
+ );
1761
case 'g':
1441
- return parseTypedArray(response, value, Float64Array, 8, obj, key);
1762
+ return parseTypedArray(
1763
+ response,
1764
+ value,
1765
+ Float64Array,
1766
+ 8,
1767
+ obj,
1768
+ key,
1769
+ arrayRoot,
1770
+ );
1771
case 'M':
1443
- return parseTypedArray(response, value, BigInt64Array, 8, obj, key);
1772
+ return parseTypedArray(
1773
+ response,
1774
+ value,
1775
+ BigInt64Array,
1776
+ 8,
1777
+ obj,
1778
+ key,
1779
+ arrayRoot,
1780
+ );
1781
case 'm':
1445
- return parseTypedArray(response, value, BigUint64Array, 8, obj, key);
1782
+ return parseTypedArray(
1783
+ response,
1784
+ value,
1785
+ BigUint64Array,
1786
+ 8,
1787
+ obj,
1788
+ key,
1789
+ arrayRoot,
1790
+ );
1791
case 'V':
1447
- return parseTypedArray(response, value, DataView, 1, obj, key);
1792
+ return parseTypedArray(
1793
+ response,
1794
+ value,
1795
+ DataView,
1796
+ 1,
1797
+ obj,
1798
+ key,
1799
+ arrayRoot,
1800
+ );
1801
case 'B': {
1802
// Blob
1803
const id = parseInt(value.slice(2), 16);
@@ -1455,8 +1808,6 @@ function parseModelString(
1808
const backingEntry: Blob = (response._formData.get(blobKey): any);
1809
return backingEntry;
1810
}
1458
- }
1459
- switch (value[1]) {
1811
case 'R': {
1812
return parseReadableStream(response, value, undefined, obj, key);
1813
}
@@ -1472,16 +1823,30 @@ function parseModelString(
1823
}
1824
// We assume that anything else is a reference ID.
1825
const ref = value.slice(1);
1475
- return getOutlinedModel(response, ref, obj, key, createModel);
1826
+ return getOutlinedModel(response, ref, obj, key, arrayRoot, createModel);
1827
+ }
1828
+ if (arrayRoot !== null) {
1829
+ bumpArrayCount(arrayRoot, value.length, response);
1830
}
1831
return value;
1832
}
1833
1834
+const DEFAULT_MAX_ARRAY_NESTING = 1000000;
1835
+
1836
+// Limit BigInt size to prevent CPU exhaustion from parsing very large values.
1837
+// 300 digits covers most practical use cases (even 512-bit integers need only
1838
+// ~154 digits) and aligns with the implicit limit from the Number approximation
1839
+// checks in fulfillReference and getOutlinedModel.
1840
+const MAX_BIGINT_DIGITS = 300;
1841
+
1842
+export const MAX_BOUND_ARGS = 1000;
1843
+
1844
export function createResponse(
1845
bundlerConfig: ServerManifest,
1846
formFieldPrefix: string,
1847
temporaryReferences: void | TemporaryReferenceSet,
1848
backingFormData?: FormData = new FormData(),
1849
+ arraySizeLimit?: number = DEFAULT_MAX_ARRAY_NESTING,
1850
): Response {
1851
const chunks: Map<number, SomeChunk<any>> = new Map();
1852
const response: Response = {
@@ -1492,6 +1857,8 @@ export function createResponse(
1857
_closed: false,
1858
_closedReason: null,
1859
_temporaryReferences: temporaryReferences,
1860
+ _rootArrayContexts: new WeakMap(),
1861
+ _arraySizeLimit: arraySizeLimit,
1862
};
1863
return response;
1864
}
packages/react-server/src/ReactFlightServer.js
+13
@@ -557,6 +557,8 @@ type DeferredDebugStore = {
557
existing: Map<ReactClientReference | string, number>,
558
};
559
560
+const __PROTO__ = '__proto__';
561
+
562
const OPENING = 10;
563
const OPEN = 11;
564
const ABORTING = 12;
@@ -3447,6 +3449,17 @@ function renderModelDestructive(
3449
// Set the currently rendering model
3450
task.model = value;
3451
3452
+ if (__DEV__) {
3453
+ if (parentPropertyName === __PROTO__) {
3454
+ callWithDebugContextInDEV(request, task, () => {
3455
+ console.error(
3456
+ 'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
3457
+ describeObjectForErrorMessage(parent, parentPropertyName),
3458
+ );
3459
+ });
3460
+ }
3461
+ }
3462
+
3463
// Special Symbol, that's very common.
3464
if (value === REACT_ELEMENT_TYPE) {
3465
return '$';
scripts/error-codes/codes.json
+12
-1
@@ -555,5 +555,16 @@
555
"567": "Already initialized stream.",
556
"568": "Already initialized typed array.",
557
"569": "Cannot have cyclic thenables.",
558
- "570": "Invalid reference."
558
+ "570": "Invalid reference.",
559
+ "571": "Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.",
560
+ "572": "Already initialized Map.",
561
+ "573": "Already initialized Set.",
562
+ "574": "Invalid forward reference.",
563
+ "575": "Invalid Map initializer.",
564
+ "576": "Invalid Set initializer.",
565
+ "577": "Invalid Iterator initializer.",
566
+ "578": "Already initialized Iterator.",
567
+ "579": "Invalid data for bytes stream.",
568
+ "580": "Server Function has too many bound arguments. Received %s but the limit is %s.",
569
+ "581": "BigInt is too large. Received %s digits but the limit is %s."
570
}