Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
↑↓navigate↵open⌘Jpreview
On this page

OpenRPC Handler

Use OpenRPCHandler to serve oRPC procedures as a JSON-RPC 2.0 API, for OpenRPC Link or any other JSON-RPC client.

Overview

OpenRPCHandler serves your router as a JSON-RPC 2.0 API over HTTP POST, for OpenRPC Link or any other JSON-RPC client. Pair it with the OpenRPC Generator to describe the API in an OpenRPC document.

npm install @orpc/openrpc@beta
pnpm add @orpc/openrpc@beta
yarn add @orpc/openrpc@beta
bun add @orpc/openrpc@beta
nub add @orpc/openrpc@beta
aube add @orpc/openrpc@beta
import { OpenRPCHandler } from '@orpc/openrpc/fetch' // or '@orpc/openrpc/node', '@orpc/openrpc/fastify', '@orpc/openrpc/aws-lambda'

const handler = new OpenRPCHandler(router)

export async function fetch(request: Request) {
  const { matched, response } = await handler.handle(request, {
    prefix: '/rpc',
    context: {}, // Provide initial context if needed
  })

  if (matched) {
    return response
  }

  return new Response('Not Found', { status: 404 })
}

The handler answers POST requests to the prefix path itself, here /rpc, so mount it on a route that matches that path in your adapter. Other requests are not matched, so other handlers or plugins can serve them. Requests must use Content-Type: application/json (or application/json-rpc), otherwise they are rejected with 415, which also protects the endpoint from CSRF.

curl -X POST http://localhost:3000/rpc \
  -H 'content-type: application/json' \
  -d '{"jsonrpc": "2.0", "method": "planet.find", "params": {"id": 1}, "id": 1}'

Methods and Params

Each procedure is a method named after its path joined by ., such as planet.find. Names starting with rpc. are reserved by JSON-RPC.

params Procedure input
{ "id": 1 } { id: 1 } (by-name)
[x] x (by-position), for inputs that are not objects
absent, [], or {} undefined or {}, whichever the input schema accepts
[x, y] rejected with -32602, since a procedure takes a single input

Results and error data are serialized like OpenAPI responses, so a Date becomes an ISO string, and an undefined result is sent as null. Use the Smart Coercion Plugin to restore native types from params.

Error Codes

An ORPCError is answered with a JSON-RPC error whose data carries the oRPC error, so OpenRPC Link restores it, including typesafe errors:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32004,
    "message": "Planet not found",
    "data": { "code": "NOT_FOUND", "defined": true, "data": { "id": 2 } }
  }
}

Error codes come from COMMON_JSON_RPC_ERROR_CODE_MAP: BAD_REQUEST, including input validation errors, is -32602 “Invalid params”, other 4xx errors are -32000 - (status - 400), such as -32004 for NOT_FOUND, 5xx errors are -32603 “Internal error”, and codes missing from the map are -32000. Use errorCodeMap to change it, and pass the same map to the OpenRPC Generator:

import { COMMON_JSON_RPC_ERROR_CODE_MAP } from '@orpc/openrpc'

const handler = new OpenRPCHandler(router, {
  errorCodeMap: { ...COMMON_JSON_RPC_ERROR_CODE_MAP, OUT_OF_STOCK: 1001 },
})

Requests that are not valid JSON-RPC are answered with the protocol errors -32700 “Parse error”, -32600 “Invalid Request”, or -32601 “Method not found”. JSON-RPC errors use HTTP status 200.

Batch Requests and Notifications

A JSON array of requests is a batch: its calls run concurrently and the responses keep the request order. A request without id is a notification: it runs to completion, but is never answered, and a request with only notifications is answered with 204. Batches are limited to maxBatchSize requests (default 100), larger ones are rejected with 413.

const handler = new OpenRPCHandler(router, {
  maxBatchSize: 20,
})

WebSocket

OpenRPCHandler from @orpc/openrpc/websocket serves the same API over a WebSocket connection, for the same WebSocket implementations as the WebSocket Adapters. Each message is a request or a batch, answered with a message on the same socket, and notifications are not answered:

import { WebSocketServer } from 'ws'
import { OpenRPCHandler } from '@orpc/openrpc/websocket'

const handler = new OpenRPCHandler(router)

const wss = new WebSocketServer({ port: 8080 })

wss.on('connection', (ws) => {
  handler.upgrade(ws, {
    context: {}, // Provide initial context if needed, resolved for each message
  })
})

Responses to concurrent calls can arrive in any order, matched to their requests by id. When the socket closes, the signal of its in-flight calls is aborted. Use .message(ws, data) and .close(ws) instead of upgrade when your server hands you the messages, such as with Bun, Cloudflare Hibernation WebSocket, or crossws:

import crossws from 'crossws/adapters/node' // any crossws adapter is supported

const ws = crossws({
  hooks: {
    message: async (peer, message) => {
      await handler.message(peer, message.text())
    },
    close: (peer) => {
      handler.close(peer)
    },
  },
})

Interceptors

Each JSON-RPC call runs as its own sub-request: interceptors and client interceptors, including those of plugins, run once per call, and each call gets its own tracing span. Use getJsonRpcCall to read the call:

import { getJsonRpcCall } from '@orpc/openrpc/standard'

const handler = new OpenRPCHandler(router, {
  interceptors: [
    async ({ next, request }) => {
      const call = getJsonRpcCall(request) // { method: 'planet.find', id: 1 }
      return next()
    },
  ],
})

Routing interceptors run once per HTTP request (or WebSocket message), around all the calls of a batch, and can answer the whole request, such as to require authentication. Over WebSocket, such an answer is sent as an error for each call of the message. rpc.discover, answered by the discovery option, only runs inside routing interceptors. Plugins built on them apply to the HTTP request itself, such as CORS, Request Limit, and Response Headers, and logging plugins such as Pino log once per HTTP request. The response headers of the calls in a batch are merged. The Batch Plugin is not needed, since JSON-RPC batches requests natively.

Last updated on

Was this page helpful?