Skip to content

Conversation

@wszgrcy
Copy link

@wszgrcy wszgrcy commented Oct 6, 2025

Closes #

🎯 Changes

What changes are made in this PR? Is it a feature or a bug fix?
fastify form-data type
https://trpc.io/docs/server/non-json-content-types#formdata-input

✅ Checklist

  • I have followed the steps listed in the Contributing guide.
  • If necessary, I have added documentation related to the changes made.
  • I have added or updated the tests related to the changes made.
image

I found that due to the version issue of undici, FormData validation may fail So should we update the official version before merging?
Or after updating, submit another patch to fix the test?

Summary by CodeRabbit

  • New Features

    • Added first-class support for multipart/form-data requests, enabling FormData inputs (e.g., for file uploads) to be passed through and processed reliably with Fastify-based servers.
    • Improved compatibility for streaming request bodies, ensuring FormData can be forwarded without JSON parsing.
  • Tests

    • Introduced end-to-end tests for a FormData-based procedure, validating multipart handling and response data.
    • Demonstrated client usage by routing FormData via a non-batched HTTP link to ensure correct multipart behavior.

@wszgrcy wszgrcy requested a review from a team as a code owner October 6, 2025 15:05
@vercel
Copy link

vercel bot commented Oct 6, 2025

@wszgrcy is attempting to deploy a commit to the trpc Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 6, 2025

Walkthrough

Introduces multipart/form-data passthrough in the Fastify tRPC plugin, adds IncomingMessage body passthrough in the Node HTTP adapter, and updates tests to include a FormData-based procedure with client-side link splitting between httpLink (for FormData) and httpBatchStreamLink.

Changes

Cohort / File(s) Summary
Fastify multipart handling
packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts
Removes existing multipart parser and registers a no-op multipart/form-data parser to pass raw body, alongside application/json handling.
Node HTTP body passthrough
packages/server/src/adapters/node-http/incomingMessageToRequest.ts
In createBody, if req.body is an IncomingMessage, return it directly (for FormData), before JSON fallback.
Tests and client link split
packages/tests/server/adapters/fastify.test.ts
Adds multipartForm procedure accepting FormData; introduces splitLink routing: FormData → httpLink, others → httpBatchStreamLink; adds test asserting multipart behavior.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Split as splitLink
  participant HL as httpLink (multipart)
  participant HB as httpBatchStreamLink
  participant F as Fastify TRPC Plugin
  participant R as tRPC Router

  Note over C,Split: Client calls mutation

  C->>Split: mutate(input)
  alt input is FormData
    Split->>HL: forward request (multipart/form-data)
    HL->>F: HTTP request with FormData body
    Note over F: Multipart parser is no-op (raw passthrough)
    F->>R: pass raw body to procedure
    R-->>F: result ({ id })
    F-->>HL: HTTP response
    HL-->>C: result
  else non-FormData
    Split->>HB: batch/stream request
    HB->>F: batched request
    F->>R: handle as usual
    R-->>F: result
    F-->>HB: response
    HB-->>C: result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble bytes like clover sprigs, hooray!
A form arrives—multipart buffet!
Fastify says, “Pass it through,”
Node whispers, “Stream will do.”
I thump, submit, the tests all cheer—
An id pops out, as crisp as ear. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description includes the template headings and checklist but has not been populated with any details under "Closes #" or the "🎯 Changes" section, leaving the key change summary blank, and instead contains an image and a question about undici version without summarizing the actual code changes. Please add the issue number or remove the "Closes #" line if no issue is being closed, fill in the "🎯 Changes" section with a concise summary of the bug fix for multipart/form-data support in the Fastify plugin, and clearly state whether this PR is a feature addition or a bug fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly indicates that this PR addresses a fix related to Fastify form-data handling, aligning with the main changes around multipart/form-data support in the plugin.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 6, 2025

Open in StackBlitz

@trpc/client

npm i https://pkg.pr.new/trpc/trpc/@trpc/client@6974

@trpc/next

npm i https://pkg.pr.new/trpc/trpc/@trpc/next@6974

@trpc/react-query

npm i https://pkg.pr.new/trpc/trpc/@trpc/react-query@6974

@trpc/server

npm i https://pkg.pr.new/trpc/trpc/@trpc/server@6974

@trpc/tanstack-react-query

npm i https://pkg.pr.new/trpc/trpc/@trpc/tanstack-react-query@6974

@trpc/upgrade

npm i https://pkg.pr.new/trpc/trpc/@trpc/upgrade@6974

commit: 93a248a

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts (1)

49-56: Global multipart parser override: verify side effects and add idempotency guard

This replaces the instance-wide multipart/form-data parser with a passthrough stream, which can impact non‑tRPC routes or apps using @fastify/multipart. Confirm ordering and scope are acceptable, or add an idempotency check.

Consider:

-  fastify.removeContentTypeParser('multipart/form-data');
+  if ('hasContentTypeParser' in fastify && fastify.hasContentTypeParser('multipart/form-data')) {
+    fastify.removeContentTypeParser('multipart/form-data');
+  }

Operational note: with a passthrough stream, Fastify’s bodyLimit on parser aggregation won’t apply; ensure size guarding is enforced downstream (e.g., in incomingMessageToRequest).

packages/tests/server/adapters/fastify.test.ts (2)

125-137: Harden FormData validation and drop as any

Use a type‑predicate with Symbol.toStringTag/Object.prototype.toString; then access input.get without casting.

Apply this diff:

-    multipartForm: publicProcedure
-      .input(
-        z.custom<FormData>((input) => {
-          // input instanceof FormData return false but is a FormData type
-          // maybe undici version
-          return input.toString() === '[object FormData]';
-        }),
-      )
-      .mutation(async ({ input }) => {
-        const result = {} as Record<string, any>;
-        result['id'] = (input as any).get('id');
-        return result;
-      }),
+    multipartForm: publicProcedure
+      .input(
+        z.custom<FormData>((input): input is FormData => {
+          // Robust cross‑impl detection; avoids undici version mismatches
+          const tag = (input as any)?.[Symbol.toStringTag];
+          return tag === 'FormData' || Object.prototype.toString.call(input) === '[object FormData]';
+        }),
+      )
+      .mutation(async ({ input }) => {
+        return { id: input.get('id') as string };
+      }),

246-258: Make FormData detection in splitLink resilient

instanceof FormData can fail across undici/globals. Use a tag/name fallback to ensure FormData routes to httpLink.

Apply this diff:

-        false: splitLink({
-          condition(op) {
-            return op.input instanceof FormData;
-          },
+        false: splitLink({
+          condition(op) {
+            const v = op.input as any;
+            return (
+              (typeof FormData !== 'undefined' && v instanceof FormData) ||
+              v?.[Symbol.toStringTag] === 'FormData' ||
+              Object.prototype.toString.call(v) === '[object FormData]'
+            );
+          },
           true: httpLink({
             url: `http://${host}`,
             headers: opts.headers,
           }),
           false: httpBatchStreamLink({
             url: `http://${host}`,
             headers: opts.headers,
           }),
         }),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd64ab3 and 93a248a.

📒 Files selected for processing (3)
  • packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts (1 hunks)
  • packages/server/src/adapters/node-http/incomingMessageToRequest.ts (2 hunks)
  • packages/tests/server/adapters/fastify.test.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/coding-guidelines.mdc)

**/*.{ts,tsx}: Always use import type for type-only imports
Separate type imports from value imports
Avoid overzealous object destructuring; prefer direct property access
Object destructuring is acceptable when a variable is used 3+ times
Prefer array destructuring
Avoid sparse array destructuring
Never destructure in function parameter declarations
Avoid destructuring potentially nullish nested properties
Maximum 3 parameters per function; use options objects when more
Type parameter names must match /^(T|$)(A-Z)?[0-9]*$/
Prefix unused variables, parameters, and caught errors with _
Prefer namespace imports for validation libraries and large modules (e.g., import * as z from 'zod', import * as React from 'react')
Follow import order: test helpers, tRPC test imports, third-party, then relative
Never import from @trpc/*/src; import from the package root instead
Do not use Symbol.dispose or Symbol.asyncDispose; use makeResource()/makeAsyncResource()
Always use await using for resource cleanup
Prefer makeResource()/makeAsyncResource() over manual disposal logic
Avoid non-null assertions (!)
Use proper type guards and optional chaining instead of non-null assertions
Switch statements must be exhaustive for union types
Rely on TypeScript inference; avoid unnecessary explicit return/output types
Use explicit types at public API boundaries, for complex generic constraints, or when inference is insufficient/ambiguous
Use the satisfies operator to retain inference while enforcing shapes
Use as const for literal type inference when appropriate
Prefer explicit typing over any
Use type assertions sparingly
Leverage TypeScript strict mode features

Files:

  • packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts
  • packages/server/src/adapters/node-http/incomingMessageToRequest.ts
  • packages/tests/server/adapters/fastify.test.ts
**/*.{ts,tsx,md,mdx}

📄 CodeRabbit inference engine (.cursor/rules/coding-guidelines.mdc)

Use camelCase for file names (with exceptions like TRPC/RPC/HTTP/JSON acronyms, .config.js, .d.ts, and tests)

Files:

  • packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts
  • packages/server/src/adapters/node-http/incomingMessageToRequest.ts
  • packages/tests/server/adapters/fastify.test.ts
packages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/coding-guidelines.mdc)

No console.log in packages; use proper logging instead

Files:

  • packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts
  • packages/server/src/adapters/node-http/incomingMessageToRequest.ts
  • packages/tests/server/adapters/fastify.test.ts
**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/test-patterns.mdc)

**/*.test.ts: ALWAYS use await using ctx = testServerAndClientResource(...) in tests that need both server and client setup
NEVER use the deprecated routerToServerAndClientNew() in tests
Import testServerAndClientResource from @trpc/client/__tests__/testClientResource
Create fresh mock instances per test using a factory (e.g., getMockFetch())
Do not use global mocks that persist across tests
Configure mocks via the client callback in testServerAndClientResource options
Use ctx.client from the test resource for making tRPC calls
Access server URLs via ctx.httpUrl and ctx.wssUrl
Configure server options via the server property in testServerAndClientResource options
Configure client options via the client callback in testServerAndClientResource options
Use descriptive test names that explain the behavior under test
Focus test names on what the test validates, not just what it does
Use proper TypeScript typing for mocks
Clear mocks between tests when needed
Use factory functions for mock creation to ensure isolation

Files:

  • packages/tests/server/adapters/fastify.test.ts
{**/*.test.{ts,tsx},**/__tests__/**/*.{ts,tsx}}

📄 CodeRabbit inference engine (.cursor/rules/coding-guidelines.mdc)

{**/*.test.{ts,tsx},**/__tests__/**/*.{ts,tsx}}: Do not use Testing Library waitFor; use vi.waitFor instead
Tests may use non-null assertions
Tests may have unused variables
Tests may allow floating promises

Files:

  • packages/tests/server/adapters/fastify.test.ts
🧬 Code graph analysis (1)
packages/tests/server/adapters/fastify.test.ts (3)
packages/client/src/links/splitLink.ts (1)
  • splitLink (9-30)
packages/client/src/links/httpLink.ts (1)
  • httpLink (75-142)
packages/client/src/links/httpBatchStreamLink.ts (1)
  • httpBatchStreamLink (23-186)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E-tests (next-formdata)
  • GitHub Check: E2E-tests (soa)
  • GitHub Check: E2E-tests (cloudflare-workers)
  • GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 20.x)
  • GitHub Check: E2E-tests (.test/ssg)
  • GitHub Check: E2E-tests (express-minimal)
  • GitHub Check: E2E-tests (.experimental/next-app-dir)
  • GitHub Check: E2E-tests (.test/diagnostics-big-router)
  • GitHub Check: e2e-legacy-node (next-prisma-starter, 18.x)
  • GitHub Check: e2e-legacy-node (next-prisma-todomvc, 20.x)
  • GitHub Check: e2e-legacy-node (next-prisma-todomvc, 18.x)
  • GitHub Check: E2E-tests (.test/internal-types-export)
  • GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 18.x)
  • GitHub Check: e2e-legacy-node (next-prisma-starter, 20.x)
  • GitHub Check: Test a monorepo using built declaration files
  • GitHub Check: Release using pkg.pr.new
  • GitHub Check: E2E-tests (Deno) (deno-deploy)
  • GitHub Check: typecheck-www
  • GitHub Check: Lint and auto-fix
  • GitHub Check: test
🔇 Additional comments (3)
packages/server/src/adapters/node-http/incomingMessageToRequest.ts (1)

2-2: LGTM on runtime IncomingMessage import

Enables a reliable instanceof check for body streams.

packages/tests/server/adapters/fastify.test.ts (2)

10-10: LGTM: httpLink import

Required for routing FormData requests outside batching.


474-483: LGTM: new multipart/form-data e2e test

Validates the new path and client link routing.

Comment on lines +25 to +28
// formData use
if (req.body instanceof IncomingMessage) {
return req.body as any;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not bypass maxBodySize for IncomingMessage bodies

Returning the raw Node stream skips the existing size check, weakening request size guarantees and risking DoS. Wrap it in a ReadableStream with the same maxBodySize guard as the fallback path.

Apply this diff:

-    // formData use
-    if (req.body instanceof IncomingMessage) {
-      return req.body as any;
-    }
+    // formData/raw stream passthrough with size guard
+    if (req.body instanceof IncomingMessage) {
+      const nodeStream = req.body;
+      let size = 0;
+      let hasClosed = false;
+      return new ReadableStream({
+        start(controller) {
+          const onData = (chunk: Buffer) => {
+            size += chunk.length;
+            if (opts.maxBodySize != null && size > opts.maxBodySize) {
+              hasClosed = true;
+              controller.error(new TRPCError({ code: 'PAYLOAD_TOO_LARGE' }));
+              nodeStream.off('data', onData);
+              nodeStream.off('end', onEnd);
+              nodeStream.off('error', onError);
+              try { nodeStream.destroy(); } catch {}
+              return;
+            }
+            controller.enqueue(
+              new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
+            );
+          };
+          const onEnd = () => {
+            if (hasClosed) return;
+            hasClosed = true;
+            nodeStream.off('data', onData);
+            nodeStream.off('end', onEnd);
+            nodeStream.off('error', onError);
+            controller.close();
+          };
+          const onError = (err: unknown) => {
+            if (hasClosed) return;
+            hasClosed = true;
+            nodeStream.off('data', onData);
+            nodeStream.off('end', onEnd);
+            nodeStream.off('error', onError);
+            controller.error(err);
+          };
+          nodeStream.on('data', onData);
+          nodeStream.on('end', onEnd);
+          nodeStream.on('error', onError);
+        },
+        cancel() {
+          try { nodeStream.destroy(); } catch {}
+        },
+      });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// formData use
if (req.body instanceof IncomingMessage) {
return req.body as any;
}
// formData/raw stream passthrough with size guard
if (req.body instanceof IncomingMessage) {
const nodeStream = req.body;
let size = 0;
let hasClosed = false;
return new ReadableStream({
start(controller) {
const onData = (chunk: Buffer) => {
size += chunk.length;
if (opts.maxBodySize != null && size > opts.maxBodySize) {
hasClosed = true;
controller.error(new TRPCError({ code: 'PAYLOAD_TOO_LARGE' }));
nodeStream.off('data', onData);
nodeStream.off('end', onEnd);
nodeStream.off('error', onError);
try { nodeStream.destroy(); } catch {}
return;
}
controller.enqueue(
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
);
};
const onEnd = () => {
if (hasClosed) return;
hasClosed = true;
nodeStream.off('data', onData);
nodeStream.off('end', onEnd);
nodeStream.off('error', onError);
controller.close();
};
const onError = (err: unknown) => {
if (hasClosed) return;
hasClosed = true;
nodeStream.off('data', onData);
nodeStream.off('end', onEnd);
nodeStream.off('error', onError);
controller.error(err);
};
nodeStream.on('data', onData);
nodeStream.on('end', onEnd);
nodeStream.on('error', onError);
},
cancel() {
try { nodeStream.destroy(); } catch {}
},
});
}
🤖 Prompt for AI Agents
In packages/server/src/adapters/node-http/incomingMessageToRequest.ts around
lines 25 to 28, returning the raw IncomingMessage bypasses the maxBodySize
check; instead wrap the IncomingMessage in a ReadableStream that enforces the
same maxBodySize guard used by the fallback path: create a ReadableStream that
reads chunks from the IncomingMessage, track total bytes read, if total exceeds
maxBodySize abort/close the stream and throw the same MaxBodySize error used
elsewhere, propagate and handle stream errors, and ensure the resulting
ReadableStream is returned in place of the raw req.body so all request bodies go
through the same size enforcement.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant