Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Tmp File Upload Plugin

Stream large file uploads into temporary files instead of memory, so requests far larger than available memory are parsed safely.

Installation

npm install @orpc/node@beta
pnpm add @orpc/node@beta
yarn add @orpc/node@beta
bun add @orpc/node@beta

Setup

Use TmpFileUploadHandlerPlugin to parse file uploads into temporary files. Bodies the standard parser would buffer into an in-memory File, and multipart/form-data file parts, stream to disk instead. Every other body is left to the standard parser.

import { TmpFileUploadHandlerPlugin } from '@orpc/node'
import { RPCHandler } from '@orpc/server/node'

const handler = new RPCHandler(router, {
  plugins: [
    new TmpFileUploadHandlerPlugin({
      /**
       * The directory temporary files are created under. Each request that
       * spools an upload gets its own subdirectory inside it, removed when
       * the request finishes.
       *
       * @default os.tmpdir()
       */
      tmpDir: './uploads',
    }),
  ],
})

Working with Uploaded Files

Procedures receive ordinary File instances and read them lazily from disk, in constant memory. Each one is a TmpFile exposing the path of its backing file, so an upload can be kept with a cheap rename instead of a copy:

import { TmpFile } from '@orpc/node'
import { rename } from 'node:fs/promises'

const uploadVideo = os
  .input(z.object({ video: z.file() }))
  .handler(async ({ input }) => {
    if (input.video instanceof TmpFile) {
      await rename(input.video.path, `./videos/${crypto.randomUUID()}`)
    }
  })

Limiting Body Sizes

Use maxBodySize to limit each kind of request body by what it actually costs. All three kinds are required together, so none is left unbounded by accident; set a kind to Number.POSITIVE_INFINITY to deliberately leave it unlimited. A body over its limit rejects with PAYLOAD_TOO_LARGE.

const handler = new RPCHandler(router, {
  plugins: [
    new TmpFileUploadHandlerPlugin({
      maxBodySize: {
        /**
         * Content parsed into memory: JSON, URL-encoded forms, and the
         * plain fields of a multipart body. Usually the lowest limit.
         */
        memory: 1024 * 1024,

        /**
         * Content streamed into temporary files: file bodies and the
         * file parts of a multipart body combined.
         */
        file: 10 * 1024 * 1024 * 1024,

        /**
         * Content consumed as a stream: event streams and raw binary
         * streams, enforced while the stream is consumed. Usually the
         * highest limit.
         */
        stream: Number.POSITIVE_INFINITY,
      },
    }),
  ],
})

A multipart body splits across the first two limits, fields against memory and file parts against file, and as a whole, framing included, it is bounded by the sum of both. A declared Content-Length over the applicable limit rejects immediately, and enforcement continues while the body streams in, so a lying length cannot bypass it.

With all three limits configured, the plugin subsumes the Request Limit Plugin. When the Request Compression Plugin is present, limits apply to the decompressed payload rather than the compressed wire size.

Learn More

For implementation details, see the source code.

Last updated on August 15, 2026

Was this page helpful?