#7180·trpc

bug: when `serializer` in `httpLink` can not return `FormData`, even though tRPC can serialize it when using in `input` directly

Author: lucacasonatoCreated Feb 21, 2026Updated Jul 21, 2026
Labels✅ accepted-PRs-welcome

Provide environment information

  System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 20.19.1 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.8.2 - /usr/local/bin/npm
    pnpm: 8.15.6 - /usr/local/bin/pnpm
  npmPackages:
    @tanstack/react-query: ^5.80.3 => 5.90.21 
    @trpc/client: canary => 11.9.1-canary.9+d92cc45b3 
    @trpc/next: canary => 11.9.1-canary.9+d92cc45b3 
    @trpc/react-query: canary => 11.9.1-canary.9+d92cc45b3 
    @trpc/server: canary => 11.9.1-canary.9+d92cc45b3 
    next: ^15.3.8 => 15.5.12 
    react: ^19.1.0 => 19.2.4 
    typescript: ^5.9.2 => 5.9.3 

Describe the bug

I have written the following serializer/deserializer pair for use with httpLink. It allows serializing Blob objects contained inside of a plain object to be serialized as FormData fields. When you pass it an object { file: new Blob(["foo"]) }, it returns a formdata with field data set to {"file": <pointer to file 0>} and field blob set to new Blob(["foo"]).

If I pass this FormData object directly to a tRPC mutation, it gets correctly serialized. In this case I have to manually deserialize it on the server though. If I instead pass this function as a serializer to httpLink, the FormData returned from serializer is encoded as JSON (resulting in {} being the value sent). This obviously then breaks things.

Serializer/deserializer for Blob
bash
import { parse, registerCustom, stringify } from "superjson";

let BLOBS: Blob[] = [];

registerCustom({
  isApplicable: (v) => v instanceof Blob,
  serialize: (blob) => {
    BLOBS.push(blob);
    return { index: BLOBS.length - 1 };
  },
  deserialize: (obj) => BLOBS[obj.index],
}, "Blob");

export function serialize(data: unknown): FormData | string {
  BLOBS = [];
  try {
    const serialized = stringify(data);
    if (BLOBS.length === 0) return serialized;
    const formData = new FormData();
    formData.append("data", serialized);
    for (const blob of BLOBS) formData.append("file", blob);
    console.log(formData);
    return formData;
  } finally {
    BLOBS = [];
  }
}

export function deserialize(val: FormData | string): unknown {
  let serialized: string;
  if (typeof val === "string") {
    serialized = val;
    BLOBS = [];
  } else {
    console.log(val);
    const data = val.get("data");
    if (typeof data !== "string") throw new Error("Expected 'data' field to be a string");
    const files = val.getAll("file");
    for (const file of files) {
      if (!(file instanceof Blob)) throw new Error("Expected 'file' field to be a Blob");
    }
    serialized = data;
    BLOBS = files as Blob[];
  }
  try {
    return parse(serialized);
  } finally {
    BLOBS = [];
  }
}

Link to reproduction

https://stackblitz.com/edit/trpc-serializing-formdata?file=src%2Fpages%2Findex.tsx

To reproduce

Press button in the demo above.

Additional information

My proposal is to allow serializer to return FormData, and to call deserializer even with FormData values from the wire. The latter is a breaking change, and would require a major version bump.

For now, I suggest a new option in the transformer called deserializeNonJsonTypes that defaults to false, but can be set to true to enable this behaviour.

For consistency, serializer should probable be called also for FormData inputs (right now it is not). This is again a breaking change, so should be flagged by a serializeNonJsonTypes option.

And furthermore I am saying FormData, but it should probably apply equally to all values that isNonJsonSerializable returns true for.

‍‍ Contributing

  • ‍♂️ Yes, I'd be down to file a PR fixing this bug!