Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/start/framework/react/guide/server-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,123 @@ export const submitForm = createServerFn({ method: 'POST' })
})
```

## Raw Handler for Streaming Uploads

For advanced use cases like streaming file uploads or accessing the raw request body, use `.rawHandler()` instead of `.handler()`. This gives you direct access to the Request object without automatic body parsing.

### Why Use Raw Handler?

The raw handler is essential when you need to:

- **Stream large file uploads** directly to cloud storage without loading them into memory
- **Enforce size limits during upload** rather than after the entire file is loaded
- **Access raw request streams** for custom body parsing (e.g., multipart/form-data with busboy)
- **Implement proper backpressure** for large data transfers
- **Minimize memory footprint** for file uploads

### Basic Raw Handler

```tsx
import { createServerFn } from '@tanstack/react-start'

export const uploadFile = createServerFn({ method: 'POST' }).rawHandler(
async ({ request, signal }) => {
// Access the raw request object
const contentType = request.headers.get('content-type')
const body = await request.text()

return new Response(
JSON.stringify({
contentType,
size: body.length,
}),
)
},
)
```

### Streaming File Upload Example

With raw handlers, you can stream files directly to cloud storage without buffering them in memory:

```tsx
import { createServerFn } from '@tanstack/react-start'

export const uploadFile = createServerFn({ method: 'POST' })
.middleware([authMiddleware]) // Middleware context is available!
.rawHandler(async ({ request, signal, context }) => {
// Access middleware context (user, auth, etc.)
const userId = context.user.id

// Access the raw request body stream
const body = request.body

if (!body) {
return new Response('No file provided', { status: 400 })
}

// Stream directly to your storage (S3, Azure, etc.)
// You can use libraries like busboy to parse multipart/form-data
// and enforce size limits DURING upload
await streamToStorage(body, {
userId,
maxSize: 25 * 1024 * 1024, // 25MB limit
signal, // Pass for cancellation support
})

return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
})
})
```

### Client-Side Usage

The raw handler works seamlessly from the client with FormData:

```tsx
import { useMutation } from '@tanstack/react-query'
import { uploadFile } from './server-functions'

function FileUpload() {
const uploadMutation = useMutation({
mutationFn: async (file: File) => {
const formData = new FormData()
formData.append('file', file)

return uploadFile({ data: formData })
},
})

return (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) uploadMutation.mutate(file)
}}
/>
)
}
```

### Key Differences from Regular Handler

| Feature | `.handler()` | `.rawHandler()` |
| ------------ | ----------------------------------- | ------------------------------ |
| Body Parsing | Automatic (FormData, JSON) | Manual - you control it |
| Memory Usage | Loads entire body into memory first | Can stream directly |
| Size Limits | Can't enforce during upload | Enforce during upload |
| Use Case | Standard data/form handling | Large files, streaming |
| Parameters | `{ data, context, signal }` | `{ request, context, signal }` |

### Important Notes

1. **No automatic body parsing** - With `rawHandler`, the request body is NOT automatically parsed. You must handle it yourself.
2. **No data parameter** - Raw handlers receive `{ request, signal, context }` instead of the usual `{ data, context, signal }`. There is no automatic data parsing/validation.
3. **Middleware context is available** - Request middleware still runs and you have full access to the middleware context (authentication, user info, etc.).
4. **Response handling** - Always return a `Response` object from raw handlers.

## Error Handling & Redirects

Server functions can throw errors, redirects, and not-found responses that are handled automatically when called from route lifecycles or components using `useServerFn()`.
Expand Down
99 changes: 98 additions & 1 deletion packages/start-client-core/src/createServerFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,80 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
const newOptions = { ...resolvedOptions, inputValidator }
return createServerFn(undefined, newOptions) as any
},
rawHandler: (...args) => {
// This function signature changes due to AST transformations
// in the babel plugin. We need to cast it to the correct
// function signature post-transformation
const [extractedFn, serverFn] = args as unknown as [
CompiledFetcherFn<Register, any>,
RawServerFn<Register, any, any>,
]

// Keep the original function around so we can use it
// in the server environment
const newOptions = {
...resolvedOptions,
extractedFn,
serverFn: serverFn as any,
rawHandler: true,
}

const resolvedMiddleware = [
...(newOptions.middleware || []),
serverFnBaseToMiddleware(newOptions),
]

// We want to make sure the new function has the same
// properties as the original function

return Object.assign(
async (opts?: CompiledFetcherFnOptions) => {
// Start by executing the client-side middleware chain
return executeMiddleware(resolvedMiddleware, 'client', {
...extractedFn,
...newOptions,
data: opts?.data as any,
headers: opts?.headers,
signal: opts?.signal,
context: {},
}).then((d) => {
if (d.error) throw d.error
return d.result
})
},
{
// This copies over the URL, function ID
...extractedFn,
__rawHandler: true,
// The extracted function on the server-side calls
// this function
__executeServer: async (opts: any, signal: AbortSignal) => {
const startContext = getStartContextServerOnly()
const serverContextAfterGlobalMiddlewares =
startContext.contextAfterGlobalMiddlewares
const ctx = {
...extractedFn,
...opts,
context: {
...serverContextAfterGlobalMiddlewares,
...opts.context,
},
signal,
request: startContext.request,
}

return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
(d) => ({
// Only send the result and sendContext back to the client
result: d.result,
error: d.error,
context: d.sendContext,
}),
)
},
},
) as any
},
handler: (...args) => {
// This function signature changes due to AST transformations
// in the babel plugin. We need to cast it to the correct
Expand Down Expand Up @@ -315,6 +389,12 @@ export type ServerFn<
ctx: ServerFnCtx<TRegister, TMethod, TMiddlewares, TInputValidator>,
) => ServerFnReturnType<TRegister, TResponse>

export type RawServerFn<TRegister, TMiddlewares, TResponse> = (ctx: {
request: Request
signal: AbortSignal
context: Expand<AssignAllServerFnContext<TRegister, TMiddlewares, {}>>
}) => ServerFnReturnType<TRegister, TResponse>

export interface ServerFnCtx<
TRegister,
TMethod,
Expand Down Expand Up @@ -356,6 +436,7 @@ export type ServerFnBaseOptions<
TResponse
>
functionId: string
rawHandler?: boolean
}

export type ValidateValidatorInput<
Expand Down Expand Up @@ -513,6 +594,9 @@ export interface ServerFnHandler<
TNewResponse
>,
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse>
rawHandler: <TNewResponse>(
fn?: RawServerFn<TRegister, TMiddlewares, TNewResponse>,
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse>
}

export interface ServerFnBuilder<TRegister, TMethod extends Method = 'GET'>
Expand Down Expand Up @@ -611,6 +695,7 @@ export type ServerFnMiddlewareOptions = {
sendContext?: any
context?: any
functionId: string
request?: Request
}

export type ServerFnMiddlewareResult = ServerFnMiddlewareOptions & {
Expand Down Expand Up @@ -718,7 +803,19 @@ function serverFnBaseToMiddleware(
},
server: async ({ next, ...ctx }) => {
// Execute the server function
const result = await options.serverFn?.(ctx as TODO)
let result: any
if (options.rawHandler) {
// For raw handlers, pass request, signal, and context
const ctxWithRequest = ctx as typeof ctx & { request: Request }
result = await (options.serverFn as any)?.({
request: ctxWithRequest.request,
signal: ctx.signal,
context: ctx.context,
})
} else {
// For normal handlers, pass the full context
result = await options.serverFn?.(ctx as TODO)
}

return next({
...ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const LookupSetup: Record<
LookupKind,
{ candidateCallIdentifier: Set<string> }
> = {
ServerFn: { candidateCallIdentifier: new Set(['handler']) },
ServerFn: { candidateCallIdentifier: new Set(['handler', 'rawHandler']) },
Middleware: {
candidateCallIdentifier: new Set(['server', 'client', 'createMiddlewares']),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export function handleCreateServerFn(
// the validator, handler, and middleware methods. Check to make sure they
// are children of the createServerFn call expression.

const validMethods = ['middleware', 'inputValidator', 'handler'] as const
const validMethods = [
'middleware',
'inputValidator',
'handler',
'rawHandler',
] as const
type ValidMethods = (typeof validMethods)[number]
const callExpressionPaths: Record<
ValidMethods,
Expand All @@ -25,6 +30,7 @@ export function handleCreateServerFn(
middleware: null,
inputValidator: null,
handler: null,
rawHandler: null,
}

const rootCallExpression = getRootCallExpression(path)
Expand Down Expand Up @@ -84,15 +90,30 @@ export function handleCreateServerFn(
// First, we need to move the handler function to a nested function call
// that is applied to the arguments passed to the server function.

const handlerFnPath = callExpressionPaths.handler?.get(
// Support both handler and rawHandler
const handlerMethod = callExpressionPaths.handler
? 'handler'
: callExpressionPaths.rawHandler
? 'rawHandler'
: null

if (!handlerMethod) {
throw codeFrameError(
opts.code,
path.node.callee.loc!,
`createServerFn must be called with a "handler" or "rawHandler" property!`,
)
}

const handlerFnPath = callExpressionPaths[handlerMethod]?.get(
'arguments.0',
) as babel.NodePath<any>

if (!callExpressionPaths.handler || !handlerFnPath.node) {
if (!handlerFnPath.node) {
throw codeFrameError(
opts.code,
path.node.callee.loc!,
`createServerFn must be called with a "handler" property!`,
`createServerFn must be called with a "handler" or "rawHandler" property!`,
)
}

Expand Down Expand Up @@ -151,6 +172,6 @@ export function handleCreateServerFn(
)

if (opts.env === 'server') {
callExpressionPaths.handler.node.arguments.push(handlerFn)
callExpressionPaths[handlerMethod]!.node.arguments.push(handlerFn)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export function createServerFnPlugin(
},
code: {
// TODO apply this plugin with a different filter per environment so that .createMiddleware() calls are not scanned in server env
// only scan files that mention `.handler(` | `.createMiddleware()`
include: [/\.\s*handler\(/, /\.\s*createMiddleware\(\)/],
// only scan files that mention `.handler(` | `.rawHandler(` | `.createMiddleware()`
include: [/\.\s*handler\(/, /\.\s*rawHandler\(/, /\.\s*createMiddleware\(\)/],
},
},
async handler(code, id) {
Expand Down
Loading