@samitouri / QOS-React-2 / commits / 10680271fa

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