diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index 0d2689840ed7..6100f4eb903f 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -21,6 +21,7 @@ import { type ServerReferenceId, } from '../client/ReactFlightClientConfigBundlerParcel'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -314,7 +315,7 @@ export function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js index 3994a6ecbf65..395727f69cfa 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js @@ -24,6 +24,7 @@ import type {Duplex} from 'stream'; import {Readable} from 'stream'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -772,7 +773,7 @@ export function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 1b2959be27e7..524944c1fb14 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -15,6 +15,7 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -313,7 +314,7 @@ function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index dfb178c742b2..85a48641326d 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -22,6 +22,7 @@ import type {Duplex} from 'stream'; import {Readable} from 'stream'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -766,7 +767,7 @@ function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js index 46222148e03a..e9edbcbdbb84 100644 --- a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js @@ -22,6 +22,7 @@ import type {Duplex} from 'stream'; import {Readable} from 'stream'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -766,7 +767,7 @@ function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 933b74e2fec6..22bb0656d72d 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -15,6 +15,7 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -313,7 +314,7 @@ function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index f2551e8c6c57..fcf32c342e72 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -22,6 +22,7 @@ import type {Duplex} from 'stream'; import {Readable} from 'stream'; +import noop from 'shared/noop'; import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -767,7 +768,7 @@ function decodeReplyFromAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } diff --git a/packages/react-server/src/ReactFlightActionServer.js b/packages/react-server/src/ReactFlightActionServer.js index 68fdc464ca9b..d00ccdfaa636 100644 --- a/packages/react-server/src/ReactFlightActionServer.js +++ b/packages/react-server/src/ReactFlightActionServer.js @@ -112,53 +112,50 @@ export function decodeAction( // the implementation details of the action data. const formData = new FormData(); - let action: Promise<(formData: FormData) => T> | null = null; - const seenActions = new Set(); + let maybeActionKey: null | string = null; // $FlowFixMe[prop-missing] body.forEach((value: string | File, key: string) => { if (!key.startsWith('$ACTION_')) { // $FlowFixMe[incompatible-type] formData.append(key, value); - return; - } - // Later actions may override earlier actions if a button is used to - // override the default form action. However, we don't expect the same - // action ref field to be sent multiple times in legitimate form data. - if (key.startsWith('$ACTION_REF_')) { - if (seenActions.has(key)) { - return; - } - seenActions.add(key); - const formFieldPrefix = '$ACTION_' + key.slice(12) + ':'; - const metaData = decodeBoundActionMetaData( - body, - serverManifest, - formFieldPrefix, - ); - action = loadServerReference(serverManifest, metaData); - return; - } - // A simple action with no bound arguments may appear twice in the form data - // if a button specifies the same action as the default form action. We only - // load the first one, as they're guaranteed to be identical. - if (key.startsWith('$ACTION_ID_')) { - if (seenActions.has(key)) { - return; - } - seenActions.add(key); - const id = key.slice(11); - action = loadServerReference(serverManifest, { - id, - bound: null, - }); - return; + } else if (key.startsWith('$ACTION_REF_')) { + // Later actions may override earlier actions if a button is used to + // override the default form action. However, we don't expect the same + // action ref field to be sent multiple times in legitimate form data. + maybeActionKey = key; + } else if (key.startsWith('$ACTION_ID_')) { + // A simple action with no bound arguments may appear twice in the form data + // if a button specifies the same action as the default form action. + maybeActionKey = key; } }); - if (action === null) { + if (maybeActionKey === null) { return null; } + const actionKey = maybeActionKey; + + let action: Promise<(formData: FormData) => T> | null = null; + if (actionKey.startsWith('$ACTION_REF_')) { + const formFieldPrefix = + '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; + const metaData = decodeBoundActionMetaData( + body, + serverManifest, + formFieldPrefix, + ); + action = loadServerReference(serverManifest, metaData); + } else if (actionKey.startsWith('$ACTION_ID_')) { + const id = actionKey.slice('$ACTION_ID_'.length); + action = loadServerReference(serverManifest, { + id, + bound: null, + }); + } else { + throw new Error('Cannot handle action key. This is a bug in React.'); + } + // Return the action with the remaining FormData bound to the first argument. return action.then(fn => fn.bind(null, formData)); } @@ -175,24 +172,28 @@ export function decodeFormState( } // Search through the form data object to get the reference id and the number // of bound arguments. This repeats some of the work done in decodeAction. - let metaData = null; + let actionKey: null | string = null; // $FlowFixMe[prop-missing] body.forEach((value: string | File, key: string) => { if (key.startsWith('$ACTION_REF_')) { - const formFieldPrefix = '$ACTION_' + key.slice(12) + ':'; - metaData = decodeBoundActionMetaData( - body, - serverManifest, - formFieldPrefix, - ); + actionKey = key; } // We don't check for the simple $ACTION_ID_ case because form state actions // are always bound to the state argument. }); - if (metaData === null) { + if (actionKey === null) { // Should be unreachable. return Promise.resolve(null); } + + const formFieldPrefix = + '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; + const metaData = decodeBoundActionMetaData( + body, + serverManifest, + formFieldPrefix, + ); + const referenceId = metaData.id; return Promise.resolve(metaData.bound).then(bound => { if (bound === null) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 9e43d54b2073..726c7dcdc159 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1404,7 +1404,7 @@ function serializeAsyncIterable( if (typeof (iterator as any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } function abortIterable() { @@ -1424,9 +1424,11 @@ function serializeAsyncIterable( enqueueFlush(request); } if (typeof (iterator as any).throw === 'function') { + // TODO: Premature exits should call return() on the iterator if it exists + // to allow cleanup. See https://tc39.es/ecma262/multipage/control-abstraction-objects.html#table-async-iterator-optional // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe[prop-missing] should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(reason).then(noop, noop); } } request.cacheController.signal.addEventListener('abort', abortIterable); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index f475821238c6..c5be606813f5 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -586,5 +586,6 @@ "598": "Maximum update depth exceeded. This could be an infinite loop. This can happen when a component repeatedly calls setState during render phase or inside useLayoutEffect, causing infinite render loop. React limits the number of nested updates to prevent infinite loops.", "599": "Expected an initialized chunk but got an initialized stream chunk instead. This payload may have been submitted by an older version of React.", "600": "A rejected Promise was passed to React without a `reason` property. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.", - "601": "A chunk pair is incomplete. This is a bug in React." + "601": "A chunk pair is incomplete. This is a bug in React.", + "602": "Cannot handle action key. This is a bug in React." }