Repository navigation
Conversation
Add @orpc/openrpc, which serves oRPC routers as a JSON-RPC 2.0 API over HTTP and WebSocket and describes them in OpenRPC documents. Streaming, files, and server-sent notifications are not supported. - OpenRPCHandler (fetch, node, fastify, aws-lambda, websocket): single calls, batches, and notifications, with the JSON-RPC protocol errors and ORPCErrors mapped to JSON-RPC error codes (errorCodeMap) - OpenRPCLink (fetch, websocket): calls any JSON-RPC 2.0 server and restores ORPCErrors, typesafe errors included - OpenRPCGenerator and openrpc() meta: OpenRPC 1.3 documents with by-name or by-position params, results, errors, components, examples, and a spec override, falling back to openapi() meta - OpenRPCDiscoveryHandlerPlugin: rpc.discover and a GET document endpoint - export OpenAPIComponentRegistry from @orpc/openapi for reuse - docs, including an OpenRPC adapters page, README tables, and tests mirroring the OpenAPI suite Refs #541 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
- Add StandardOpenRPCHandler, so the five adapters share the codec and envelope wiring
- Work out once per procedure whether its input accepts undefined or {} for empty params
- Share the result-or-error check between the envelope and isJsonRpcResponse
- Name the protocol codes in COMMON_JSON_RPC_ERROR_CODE_MAP, and drop a dead fallback entry
- Convert the output schema and read the openrpc() meta once per method
- Drop the unused @standard-server/fetch and @orpc/zod dependencies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 7a92315 | Commit Preview URL Branch Preview URL |
Oct 31 2026, 02:48 AM |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/mcp
@orpc/msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/openrpc
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ No critical issues — two minor suggestions inline.
Reviewed changes
- New package
@orpc/openrpc: JSON-RPC 2.0 server handlers (fetch, node, fastify, aws-lambda, websocket), client links (fetch, websocket), an OpenRPC 1.3 generator,openrpc()metadata, and anrpc.discoverplugin, all built on the existingStandardHandler/StandardLink. - JSON-RPC envelope plugin: parses single, batch, and notification requests, runs each call as its own sub-request (so interceptors/tracing/rate limiting run once per call), and maps protocol codes plus
ORPCErrors viaerrorCodeMap. @orpc/openapichange:OpenAPIComponentRegistryis now exported and accepts any document withcomponents.schemas(OpenAPIComponentRegistryDocument), letting the generator reuse it; OpenAPI behavior is unchanged.- Docs/wiring: new
/docs/openrpcpages plus README, comparison-table, landing-page, and skill updates.
The JSON-RPC conformance path (request validation, batches, notifications, id handling, error codes) and the WebSocket client transport were checked against the spec and the existing tests and look correct. Both findings below are edge cases, not blockers.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Two minor, non-blocking findings to accompany the review above (the inline anchors could not be attached — tooling limited this run to a single review). 1. Methods that declare no params (no Trigger: Suggested fix: only emit example params the method declares. The existing 2. WebSocket handler: a The per-socket Trigger: a binary JSON-RPC frame via For reference, the rest of the implementation checks out: the JSON-RPC request/batch/notification/id handling, the error-code mapping, the WebSocket client transport, and the generator's params/result/error/component shapes all match the spec and the tests.
|
Request ids are now base-36 strings of a BigInt counter, like @standard-server/peer, so they never lose precision after Number.MAX_SAFE_INTEGER calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the delta since the prior pullfrog review (41b08e8): one commit (5106de1) that changes how the client link generates JSON-RPC request ids.
- Base-36 string request ids:
OpenRPCLinkCodecnow draws ids from a per-codecSequentialIdGenerator(base-36 of aBigIntcounter) instead of an incrementing JS number, so they stay unique pastNumber.MAX_SAFE_INTEGER.JsonRpcIdalready accepts strings, the handler echoes the id verbatim, and the WebSocket transport matches responses by the same string key — the wire-format change is internal and spec-compliant. - Tests and docs aligned: the fetch, websocket, and standard link-codec tests and the
link.mdxexample now use string ids; the response-matching test now rejects a numericidwhen a string id was sent (correct per the spec's "same value" rule while the server still accepts both).
No open pullfrog threads were present to retire (the prior review's inline notes never landed as GitHub comments), and nothing in this commit touches the two pre-existing edge cases carried forward in the PR summary.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…, not a plugin Routing interceptors now run once per HTTP request or WebSocket message, and each call still runs through StandardHandler as its own sub-request, with its own tracing span, interceptors, and client interceptors. This replaces OpenRPCEnvelopeHandlerPlugin and its hard-coded list of plugin names, so plugins without ordering hints, and the routingInterceptors option, no longer run once per call. - A routing interceptor can answer or reject the whole request - Discovery is the handler's discovery option instead of OpenRPCDiscoveryHandlerPlugin - WebSocket messages go through handleMessage instead of a synthetic POST request, which also answers plugin errors while reading a message Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the delta since the prior pullfrog review (5106de1): one commit (276c48c) that moves the JSON-RPC wire handling into the handler and folds discovery into a handler option.
- Wire handling moved into
StandardOpenRPCHandler: the~openrpcenvelope plugin is gone; the handler now overridesStandardHandler.handle, owns the prefix/method dispatch, per-call sub-request creation, batch/notification/maxBatchSizesemantics, header merging, and theORPCError→rejectmapping. It manually runs the plugininitchain in its constructor and passes onlyinterceptors/clientInterceptorstosuper, so routing interceptors run once per HTTP request/message and each call still gets its ownStandardHandlerpass (span, interceptors, client interceptors). - Discovery via option:
OpenRPCDiscoveryHandlerPluginand the@orpc/openrpc/pluginsexport are removed;discovery: { spec, specPath? }now answersrpc.discoverandGET {prefix}/openrpc.json.speccan throwORPCErrorto refuse (GET returns the error's status;rpc.discoverreturns a JSON-RPC error at 200). - WebSocket handler:
.message()now callshandleMessage(pre-parsed message) instead of.handle()with a marked request; behaviour of the per-socket abort controller is unchanged. - Tests/docs: unit tests were reorganized (codec suite, new discovery suite, reworked handler suite using
os.lazyto exercise the single-vs-batch rethrow split), and the all-plugins OpenRPC coverage moved totests/openrpc/plugins.test.ts. Docs/README now describe the routing-intercepting layers instead of the old plugin.
I verified the full delta against StandardHandler, the plugin init/route contracts (rethrow, request-limit, cors, response-headers, ratelimit), and the WebSocket/fetch/node adapters, then ran pnpm vitest run packages/openrpc/src/adapters/standard tests/openrpc (198 passed). The context/headers/prefix propagation into per-call sub-requests checks out, and the rethrow routing-interceptor semantics (single call rethrows, batch rejects after all calls run) follow from the new flow.
The two pre-existing minor edge cases carried forward in the summary (generator params: [] example; WebSocket lazy-AbortController race on a queued Blob frame) live in lines this commit did not touch, so they are not re-raised here.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
A WebSocket client matches responses by id, so when a routing interceptor answers, declines, or a plugin rejects a whole message, each of its calls now gets the error under its own id instead of a single null-id error or no reply. - rpc.discover rethrows unexpected spec errors, like any other call - Request-level plugins sort around core's tracing plugin, as in StandardHandler, so CORS answers preflights before logging plugins see them - Document that rpc.discover only runs inside routing interceptors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
There was a problem hiding this comment.
ℹ️ No critical issues — one minor note inline.
Reviewed changes
Re-reviewed the delta since the prior pullfrog review (276c48c): one commit (9f53db8) that makes a refused WebSocket message answer each of its calls.
- Per-call refusal over WebSocket:
handleMessagenow answers each call of a message a routing interceptor answers or declines (or a plugin rejects) with the error under its ownid, via the newanswerEachCall, instead of a singlenull-id error or no reply. rpc.discoverrethrows unexpectedspecerrors:discoverno longer wraps non-ORPCErrors withtoORPCError; a singlerpc.discoverpropagates them, while a batch still answers-32603and reports an unhandled rejection.- Plugin ordering around tracing: the constructor adds a
~tracingplaceholder tosortPlugins, so plugins orderedbefore/aftercore's tracing plugin keep their relative order (for example CORS answers preflights before Pino sees them). - Docs and tests:
handler.mdxand the JSDoc document the per-call WebSocket refusal and thatrpc.discoveronly runs inside routing interceptors; tests cover the refusal paths, therpc.discoverrethrow, and the CORS/Pino ordering.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| } | ||
| catch (error) { | ||
| if (!(error instanceof ORPCError)) { | ||
| throw error |
There was a problem hiding this comment.
Rethrowing unexpected spec errors makes a single rpc.discover hang over WebSocket: handleMessage → dispatch → call rethrows for a non-batch message, so no response is ever sent and the client waits until the socket closes (plus an unhandled rejection), whereas a normal procedure's unexpected error is answered with -32603. The HTTP path is covered by the new test, but there is no WebSocket equivalent.
Technical details
# `rpc.discover` with an unexpected `spec` error never answers over WebSocket
## Affected sites
- `packages/openrpc/src/adapters/standard/openrpc-handler.ts:335-337` — `discover` rethrows a non-`ORPCError` instead of encoding it.
- `packages/openrpc/src/adapters/standard/openrpc-handler.ts:289-302` — `call` rethrows when `batch` is false, so the throw escapes `dispatch`.
- `packages/openrpc/src/adapters/standard/openrpc-handler.ts:157-190` — `handleMessage` has no catch around `intercept`, so it rejects and `OpenRPCHandler.message()` never sends a reply.
- `packages/openrpc/src/adapters/websocket/transport.ts:43-63` — a pending client call settles only on a matching `id` response or socket close (no timeout), so it hangs.
## Required outcome
- A single `rpc.discover` over WebSocket whose `spec` throws a non-`ORPCError` should still produce a reply (for example `-32603` under the call's `id`), matching how `StandardHandler` turns an unexpected procedure error into an error response rather than throwing.
## Open questions for the human
- Is rethrowing intended for the WebSocket path, or only for HTTP (where a 500 surfaces the bug)? If intended, a WS caller of `rpc.discover` has no way to observe the failure.- Refuse each call of a WebSocket message by the final answer (not matched, or a status of 400 or more), so a routing interceptor that refuses after next() is answered per call too, without a dispatched flag or a reject callback - Encode errors with the codec's encodeError instead of a second serializer and error code map - Resolve the discovery spec once for rpc.discover and the GET endpoint - Build a call's sub-request only when it runs through StandardHandler - Move the discovery tests into the handler tests, and drop duplicated tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The refactor is behavior-preserving apart from one WebSocket fix, and the 188 affected tests plus the package type-check pass locally. One prior minor thread (the
rpc.discover-over-WebSocket hang) remains open and is not addressed by these commits.
Reviewed changes
Re-reviewed the delta since the prior pullfrog review (9f53db8): one commit (dc2a367, "refactor(openrpc): simplify StandardOpenRPCHandler").
- Handler delegates JSON-RPC error encoding to the codec:
StandardOpenRPCHandlerdrops its ownserializer/errorCodeMapfields;rejectanddiscovernow callopenrpcCodec.encodeError, so error encoding lives in one place.encodeError's return type is narrowed to carrybody.error: JsonRpcErrorObject, andprotocolErroris narrowed to the error variant ofJsonRpcResponse. - WebSocket refusal logic in
handleMessagesimplified: thedispatchedflag and therejectoverride threaded intodispatchare gone.handleMessageinspects the routing result directly — unmatched →-32601per call,status >= 400→ per-call error,status < 400→ the result as-is. This also covers the previously unhandled case where a routing interceptor callsnext()and then returns an error response (new test). - Discovery split into a shared
resolveSpec:discover(POST) andserveSpec(GET) now share one helper that returns the document or theORPCErrorspecthrew. The standalone discovery test file was folded intoopenrpc-handler.test.ts. - Test adjustments: the link-codec sequential-id test no longer re-asserts the base-36 format (covered by
@orpc/shared'sid.test.ts), the discovery batch-rethrow case folds into the single-call rethrow test, and the ambiguous request-limit-413 plugin test is removed.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…ages - Add build.config.ts and run obuild on prepack - Annotate COMMON_JSON_RPC_ERROR_CODE_MAP for isolatedDeclarations - Mark side-effect-free module-level calls as pure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5

Adds
@orpc/openrpc. It serves an oRPC router as a JSON-RPC 2.0 API over HTTP and WebSocket, and describes it in an OpenRPC 1.3 document. Streaming, files, and server-sent notifications are out of scope.Refs #541
Server
OpenRPCHandlerfor fetch, node, fastify, aws-lambda, and websocket.., such asplanet.find. An object input is sent as by-name params, any other input as[x]. Empty params (absent,[], or{}) becomeundefinedor{}, whichever the input schema accepts.maxBatchSize(default 100). Notifications run but are never answered.ORPCErroris mapped to a JSON-RPC code througherrorCodeMap(defaultCOMMON_JSON_RPC_ERROR_CODE_MAP, whereBAD_REQUESTis -32602), and itsdatacarries{ code, defined, data }so clients can restore it.StandardOpenRPCHandlerextendsStandardHandlerand does the JSON-RPC wire handling itself, with no envelope plugin.StandardHandleras its own sub-request, with its own tracing span,interceptorsandclientInterceptors.getJsonRpcCall(request)reads the call.application/json(orapplication/json-rpc); anything else gets415, which also protects against CSRF.handleMessage(): each message is a request or a batch, answered on the same socket. When the socket closes, its in-flight calls are aborted..upgrade(), or.message()/.close()for Bun, Cloudflare Hibernation, uWebSockets and crossws.Client
OpenRPCLinkfor fetch and websocket.ORPCErrors, typesafe errors included.SequentialIdGenerator: base-36 strings of aBigIntcounter, as in@standard-server/peer.200, so the error'scausegets the HTTP status of its error code. That lets the Retry plugin retry only server errors.id, and connects again on the next call after the socket closes.OpenRPC document
OpenRPCGenerator:filteranderrorCodeMapoptions.openrpc()meta: summary, description, tags, examples and spec. It falls back to theopenapi()meta.discovery: { spec, specPath }option: serves the document throughrpc.discoverandGET /openrpc.json. Ifspecthrows anORPCError, both answer with that error.Change to
@orpc/openapiOpenAPIComponentRegistryis now exported, and it accepts any document withcomponents.schemas(OpenAPIComponentRegistryDocument), so the OpenRPC generator can reuse it. OpenAPI's behavior is unchanged.Docs
New pages under
/docs/openrpc: overview, handler, link, adapters, and specification.skills/orpc/SKILL.md.Known limitations
rpc.discover: it runs only inside routing interceptors, with no per-call span or interceptors.reconnect/connectOnInitoptions.Testing
packages/openrpc/tests, mirroring the OpenAPI suite.tests/openrpcrun over Hono/fetch, Node HTTP and WebSocket, plus a test that mounts 13 real handler plugins onOpenRPCHandler.These all pass locally:
pnpm vitest run packages/openrpc packages/openapi tests/openrpc tests/pluginspnpm lintpnpm type:checkpnpm docs:validatepnpm exec sherif🤖 Generated with Claude Code
https://claude.ai/code/session_01ETTyGHAmagRW7GK4kevsX5