RPC Handler
Use RPCHandler to communicate with RPC Link and other clients that implement the RPC protocol.
Overview
const handler = new RPCHandler(router, {
interceptors: [
async ({ next, path }) => {
console.time(path.join('.'))
try {
return await next()
}
catch (err) {
console.error(`${path.join('.')}:`, err)
throw err
}
finally {
console.timeEnd(path.join('.'))
}
}
],
plugins: [
new CORSHandlerPlugin()
],
})INFO
The actual usage of RPCHandler depends on the adapter you use. For example, when using the fetch adapter, the handler is used like this:
export async function fetch(request: Request) {
const { response } = await handler.fetch(request, {
prefix: '/rpc',
context: {} // <- provide initial context if needed
})
return response ?? new Response('Not Found', { status: 404 })
}WARNING
To better support Blob, File, and ReadableStream<Uint8Array> at the root level in cross-origin scenarios, extend your CORS allowlist to allow clients to send and receive the Content-Disposition and Standard-Server headers. Learn more in the Standard Server documentation. If you use the CORS Plugin, include them in allowHeaders and exposeHeaders:
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})Interceptors
Interceptors let you observe or change different stages of an RPC request. Common use cases include logging, error handling, and metrics.
Routing Interceptors
Routing interceptors run on every request before routing. Use them when you need to handle all requests, including requests that do not match a procedure.
const handler = new RPCHandler(router, {
routingInterceptors: [
async ({ next, request, context }) => {
if (condition) {
return { matched: false }
}
const { matched, response } = await next()
return { matched, response }
},
],
})Interceptors
These interceptors run only for matched requests, after routing and before error handling (but can't use ORPCError for typesafe errors). Use them when you need access to the matched procedure.
TIP
In most cases, interceptors are the best choice. They provide more context, are easier to work with, and run before error handling.
const handler = new RPCHandler(router, {
interceptors: [
async ({ next, request, procedure, context }) => {
try {
const response = await next()
return response
}
catch (err) {
if (err instanceof CustomError) {
throw new ORPCError('CUSTOM_ERROR', { message: err.message, cause: err })
}
throw err
}
},
async ({ next, path }) => {
console.time(path.join('.'))
try {
const response = await next()
return response
}
catch (err) {
console.error(`${path.join('.')}:`, err)
throw err
}
finally {
console.timeEnd(path.join('.'))
}
},
],
})Client Interceptors
Client interceptors run only for matched requests, after input decoding, before output encoding and can use ORPCError for typesafe errors. Use them when you need access to the procedure, input, and output.
const handler = new RPCHandler(router, {
clientInterceptors: [
async ({ next, input, context, procedure }) => {
const output = await next()
return output
},
],
})Adapter Interceptors
Some RPCHandler implementations, such as fetch or node adapters, also support adapter interceptors. These run before Routing Interceptors and let you work with the adapter's native request and response objects.
const handler = new RPCHandler(router, {
fetchInterceptors: [
async ({ next, request }) => {
const { matched, response } = await next()
return { matched, response }
},
],
})INFO
This example uses the fetch adapter. For other adapters, refer to their JSDoc or adapter-specific documentation.
Plugins
Plugins package reusable interceptors. For example, CORS Plugin adds a routing interceptor to handle preflight requests and adds CORS headers to every response.
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
})INFO
HTTP-based RPCHandler implementations enable the CSRF Guard Plugin by default to protect RPC requests from CSRF attacks. Disable it with csrfGuardHandlerPlugin.enabled.
const handler = new RPCHandler(router, {
csrfGuardHandlerPlugin: {
enabled: false,
},
})Custom Serializer
RPCHandler uses a built-in serializer that supports many native types. Provide a custom serializer when you need extra types or different encoding behavior. For more details, see RPC Serializer.
const handler = new RPCHandler(router, {
serializer: new RPCSerializer({
handlers: {
// ...custom handlers
},
}),
})Filtering Procedures
Use the filter option to exclude procedures from matching:
const handler = new RPCHandler(router, {
filter: (contract, path) => getIsInternalMeta(contract) !== true,
})Custom Error Response
By default, RPCHandler uses COMMON_ERROR_STATUS_MAP to determine response status codes. Use errorStatusMap to customize them:
import { COMMON_ERROR_STATUS_MAP } from '@orpc/server'
const handler = new RPCHandler(router, {
errorStatusMap: {
...COMMON_ERROR_STATUS_MAP,
CUSTOM_ERROR: 599,
},
})Common Error Status Map
| Error Code | HTTP Status Code |
|---|---|
| BAD_REQUEST | 400 |
| UNAUTHORIZED | 401 |
| PAYMENT_REQUIRED | 402 |
| FORBIDDEN | 403 |
| NOT_FOUND | 404 |
| METHOD_NOT_SUPPORTED | 405 |
| NOT_ACCEPTABLE | 406 |
| TIMEOUT | 408 |
| CONFLICT | 409 |
| GONE | 410 |
| PRECONDITION_FAILED | 412 |
| PAYLOAD_TOO_LARGE | 413 |
| UNSUPPORTED_MEDIA_TYPE | 415 |
| UNPROCESSABLE_CONTENT | 422 |
| PRECONDITION_REQUIRED | 428 |
| TOO_MANY_REQUESTS | 429 |
| CLIENT_CLOSED_REQUEST | 499 |
| INTERNAL_SERVER_ERROR | 500 |
| NOT_IMPLEMENTED | 501 |
| BAD_GATEWAY | 502 |
| SERVICE_UNAVAILABLE | 503 |
| GATEWAY_TIMEOUT | 504 |
Event Stream Options
Configure how event iterators are streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports:
const handler = new RPCHandler(router, {
toFetchResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 5000
*/
interval: 5000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
})Lifecycle
TODO: add lifecycle diagram

