Skip to content

Bug: Nested FormData in Server Action arguments is returned empty when partId >= 10 due to decimal/hex mismatch #38038

Open

Description

@sujlcodes

When passing nested FormData instances as arguments to a Server Action (encodeReply / decodeReply), any FormData object corresponding to part ID 10 or greater (i.e. refId >= 10) has all its entries silently discarded and is decoded as an empty FormData (data.entries().next().done === true).
This is caused by an integer-radix mismatch between the serializer and deserializer:

  1. In packages/react-client/src/ReactFlightReplyClient.js:707, field entries are keyed with a decimal prefix:
    const prefix = formFieldPrefix + '_' + refId + '_';
    When refId is 10, the entries are appended as formFieldPrefix + '_10_' + key.
  2. The reference token emitted by serializeFormDataReference (packages/react-client/src/ReactFlightReplyClient.js:138) is formatted as hexadecimal:
    function serializeFormDataReference(id: number): string {
      return '$K' + id.toString(16);
    }
    When refId is 10, it emits '$Ka'.
  3. In packages/react-server/src/ReactFlightReplyServer.js:1765-1773, the server slices off '$K' and reconstructs the prefix directly from the hex string without calling parseInt(..., 16):
    case 'K': {
      const stringId = value.slice(2);
      const formPrefix = response._prefix + '_' + stringId + '_';
      return consumeBackingEntries(response._formData, formPrefix);
    }
    The server looks for response._prefix + '_a_'.
  4. In consumeBackingEntries (packages/react-server/src/ReactFlightReplyBackingFormData.js:130-141), the key _10_... fails key.startsWith(formPrefix) (_a_). The loop immediately breaks, returning an empty FormData and leaving keyPointer at 0, causing subsequent FormData objects in the payload to also fail and return empty.
    Every other handler in ReactFlightReplyServer.js (case 'B', case 'Q', case 'W', case 'A', case 'R', etc.) parses the hex reference ID to integer via parseInt(..., 16) before constructing the key. case 'K' is the only handler that omitted this step.
    React version: 19.3.0 (main / commit 16a1d5d)

React version:

Steps To Reproduce

  1. Call a Server Action with a payload containing 10 or more FormData objects (or a payload with 9 other objects like Maps/Sets/Blobs followed by a FormData).
  2. Inside the server action handler, inspect Array.from(formData.entries()) or call formData.get('someKey') on the 10th FormData.
  3. Observe that items 0 to 8 retain their entries, but item 9 (refId = 10) is completely empty and returns null for all keys.
    Link to code example:
    A minimal reproduction test case added to packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js:
it('can pass the 10th or later FormData in a reply payload', async () => {
  const list = [];
  for (let i = 0; i < 12; i++) {
    const fd = new FormData();
    fd.set('key_' + i, 'value_' + i);
    list.push(fd);
  }
  const body = await ReactServerDOMClient.encodeReply(list);
  const decodedList = await ReactServerDOMServer.decodeReply(
    body,
    webpackServerMap,
  );
  expect(decodedList.length).toBe(12);
  // Fails on index 9 (refId 10):
  for (let i = 0; i < 12; i++) {
    expect(decodedList[i].get('key_' + i)).toBe('value_' + i);
  }
});

Run with:

node ./scripts/jest/jest-cli.js --testPathPatterns="ReactFlightDOMReply-test"

Link to code example:

The current behavior

  • decodedList[9].get('key_9') returns null.
  • Array.from(decodedList[9].entries()) returns [].
  • decodedList[10] and decodedList[11] also return empty due to the cursor remaining frozen.
  • Data is silently dropped with no errors or warnings thrown.

The expected behavior

  • decodedList[9].get('key_9') returns "value_9".
  • All FormData objects at index 9 and above should retain all keys and values across the serialization boundary.

Activity

  1. added a commit that references this issue on Oct 29, 2026
  2. lazerg commented on Oct 30, 2026

    @lazerg
    Contributor

    This is already fixed in #37879, which has the same parseInt(stringId, 16) change plus a test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions