Skip to content

Support streaming file uploads in server functions #5704

@gzaripov

Description

@gzaripov

Which project does this relate to?

Start

Describe the bug

Currently, TanStack Start automatically calls await request.formData() in server functions when the content-type is multipart/form-data, which loads the entire file into memory before the handler is invoked. This prevents implementing true streaming file uploads with size limit enforcement.

Reference: server-functions-handler.js:49

if (formDataContentTypes.some(
  (type) => contentType && contentType.includes(type)
)) {
  // ...
  const formData = await request.formData();  // <-- File fully loaded into memory here
  // ...
  return await action(params, signal);  // Handler called after file is in memory
}

Current Behavior

When a user uploads a large file (e.g., 100MB, 1GB, or even 10GB):

  1. The entire file is loaded into server memory via request.formData()
  2. Only then is the server function handler called
  3. File size limits cannot be enforced before memory consumption
  4. Large uploads can crash the server or cause OOM errors
  5. True streaming from network → storage is impossible

Expected Behavior

Server functions should be able to access the raw request body stream via getRequest() before TanStack Start parses the FormData, allowing developers to:

  1. Enforce file size limits during upload (reject at 25MB instead of buffering 10GB)
  2. Stream files directly from network to cloud storage (S3, Azure Blob, etc.)
  3. Implement proper backpressure handling
  4. Minimize memory footprint for file uploads

Use Case

Streaming file uploads to Azure Blob Storage with size enforcement:

export const uploadFileRpc = createServerFn({ method: 'POST' })
  .middleware([authMiddleware])
  .handler(async (ctx) => {
    const request = getRequest();
    
    // Would like to access raw body stream here
    const busboy = new Busboy({
      headers: {
        'content-type': request.headers.get('content-type') || '',
      },
      limits: {
        fileSize: 25 * 1024 * 1024, // Enforce 25MB limit DURING upload
      },
    });

    busboy.on('file', (fieldname, fileStream) => {
      // Stream directly to Azure without buffering in memory
      const webStream = Readable.toWeb(fileStream);
      await objectStorage.upload({ stream: webStream });
    });

    // Currently doesn't work because request.body is already consumed
    Readable.fromWeb(request.body).pipe(busboy);
  });

Current Workaround

The only workaround is to bypass TanStack Start server functions entirely and create separate Hono/Express routes for file uploads, which defeats the purpose of having a unified server function API.

Impact

This affects any TanStack Start application that needs to:

  • Handle large file uploads (videos, backups, datasets)
  • Implement proper file size validation
  • Stream files to cloud storage
  • Build production file upload services

Steps to Reproduce the Bug or Issue

Try to stream the file from user to other source, for example blob storage

Expected behavior

Add a configuration option or special handler type for server functions that need raw stream access:

Option 1: Skip FormData parsing when a flag is set

export const uploadFileRpc = createServerFn({ 
  method: 'POST',
  parseBody: false, // Skip automatic FormData parsing
})
  .handler(async (ctx) => {
    const request = getRequest();
    // request.body stream is still available
  });

Option 2: Provide a separate handler for raw body access

export const uploadFileRpc = createServerFn({ method: 'POST' })
  .rawBodyHandler(async (ctx, request) => {
    // Access raw request before any parsing
    // request.body stream is available here
  });

Screenshots or Videos

No response

Platform

  • @tanstack/react-start: 1.133.20
  • Runtime: Node.js with Hono

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions