jsonrpc

package module
v1.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 19 Imported by: 7

README

jsonrpc

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

jsonrpc is a transport-neutral, full JSON-RPC 2.0 server and client package. Protocol behavior is explicit, errors are auditable, middleware is composable, HTTP is optional, and malformed input is conformance- and fuzz-tested.

Status

The package exposes the stable v1 API and wire contract. This source tree retains that canonical module identity while adding bounded transport and diagnostic hardening. Production package code is held to meaningful 100% statement coverage.

Requirements

  • Go 1.27.0 or later
  • no runtime dependencies outside the standard library

Installation

go get github.com/faustbrian/go-jsonrpc@v1

The canonical v1 module path carries the hardened behavior; no /v2 migration or replacement directive is required.

Quickstart

registry := jsonrpc.NewRegistry()
err := registry.Register("math.add", func(
    ctx context.Context,
    params json.RawMessage,
) (any, error) {
    values, rpcErr := jsonrpc.DecodeParams[[]int](params)
    if rpcErr != nil || len(values) != 2 {
        return nil, jsonrpc.InvalidParams()
    }

    return values[0] + values[1], nil
})
if err != nil {
    return err
}

handler := jsonrpc.NewHTTPHandler(jsonrpc.NewDispatcher(registry))

Trusted protocol adapters can register reserved rpc.* methods explicitly with Registry.RegisterSystem; ordinary application registration continues to reject that namespace.

Use NewClient with NewHTTPTransport for client calls. The quickstart contains complete server, client, notification, and batch examples.

Package Guarantees

  • requests, notifications, and explicit null IDs remain distinct
  • string, number, and null IDs round-trip without coercion
  • standard errors use the required codes and response shapes
  • batch and notification-only behavior follows JSON-RPC 2.0
  • clients validate response shape, ID correlation, duplicates, and missing batch members
  • dispatcher and client parsing are independently resource-bounded
  • protocol dispatch remains transport-neutral
  • adapters can use Dispatcher.DispatchSingle to apply a compatible custom response envelope without decoding an already encoded dispatcher response

The root module is the only public package. The separate interoperability module is an internal, non-releasable engineering harness and is not an application dependency.

Dispatchers and the default client are safe for concurrent calls subject to the concurrency contracts of caller-supplied handlers, middleware, hooks, transports, ID generators, and HTTP clients. The package starts no background work. Callers own those collaborators and all application resources; the HTTP binding closes accepted JSON request bodies and every received response body. Bodies rejected before request admission remain owned by the HTTP server or direct caller.

Documentation

Start with the documentation index, quickstart, guide for adopting the package, and API reference. Use the conformance matrix, middleware guide, security guide, threat model, and specification decision register for production review.

Release history is maintained in CHANGELOG.md. Runnable programs live under examples. For support, use SUPPORT.md; report vulnerabilities privately through SECURITY.md. Shared construction, ownership, lifecycle, and composition expectations are in the versioned Golib ecosystem index and its Protocols and descriptions family.

Development

Use ordinary Go tests and a focused two-second fuzz smoke as described in CONTRIBUTING.md. make check and make ci use bounded local assurance with the public verifier and selected concurrency assertions. make check-full retains explicit full assurance, including the owned interoperability harness, full fuzz campaigns and benchmarks.

Contributing

Read CONTRIBUTING.md and follow the code of conduct. Protocol and public API changes require explicit compatibility analysis.

Security

Report vulnerabilities privately according to SECURITY.md. Review docs/security.md before exposing a dispatcher to untrusted clients, and use the threat model to assign transport, credential, and network-egress ownership.

License

jsonrpc is available under the MIT License. Attribution and third-party policy are recorded in NOTICE and THIRD_PARTY_NOTICES.md.

  • OpenRPC models and validates OpenRPC descriptions and integrates discovery with this package.
  • service provides application lifecycle and HTTP serving without changing JSON-RPC semantics.

Documentation navigation

Complete documentation map · Current source package map

Use the versioned ecosystem entry for package selection. The frozen compatibility cohort is separate from current package releases and unpublished source changes. Adopting them does not silently update that cohort.

Documentation

Overview

Package jsonrpc implements transport-neutral JSON-RPC 2.0 clients and servers. It provides explicit protocol envelopes, request dispatch, middleware, structured errors, strict client response validation, and thin net/http adapters without imposing a router, logger, tracer, or validator.

Requests and responses can be processed directly as bytes through Dispatcher and Transport, allowing custom transports to share the same protocol rules. The protocol is defined by the JSON-RPC 2.0 specification: https://www.jsonrpc.org/specification.

Construct dispatchers, clients and HTTP adapters with their New functions; Registry and AtomicIDGenerator also have usable zero values. Dispatch is synchronous and propagates context but cannot preempt callbacks or infer retry safety. Concurrent use requires caller-owned callbacks, transports and result targets to satisfy their own synchronization and lifetime contracts.

Standalone envelope decoding and validation are separate steps. Dispatcher and Client provide byte/item bounds; direct decoders need caller-owned input bounds. Raw messages and copied IDs can share mutable storage. Public error messages/data cross the wire; retained local causes are not a redaction service. Specification decisions and pinned example provenance live under specification/ and docs/specification-decisions.md; no HTTP binding is implied by JSON-RPC 2.0.

See the repository documentation for compatibility guarantees and complete server, client, notification, and batch examples.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http/httptest"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	registry := jsonrpc.NewRegistry()
	if err := registry.Register("add", func(_ context.Context, raw json.RawMessage) (any, error) {
		values, rpcErr := jsonrpc.DecodeParams[[]int](raw)
		if rpcErr != nil || len(values) != 2 {
			return nil, jsonrpc.InvalidParams()
		}
		return values[0] + values[1], nil
	}); err != nil {
		fmt.Println(err)
		return
	}
	server := httptest.NewServer(jsonrpc.NewHTTPHandler(jsonrpc.NewDispatcher(registry)))
	defer server.Close()
	transport, err := jsonrpc.NewHTTPTransport(server.URL)
	if err != nil {
		fmt.Println(err)
		return
	}
	result, err := jsonrpc.Call[int](context.Background(), jsonrpc.NewClient(transport), "add", []int{2, 3})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(result)
}
Output:
5

Index

Examples

Constants

View Source
const (
	// CodeRequestLimitExceeded is the implementation-defined server error used
	// when a dispatcher payload or batch exceeds its configured bound.
	CodeRequestLimitExceeded = -32000
	// CodeParseError indicates invalid JSON.
	CodeParseError = -32700
	// CodeInvalidRequest indicates a structurally invalid request.
	CodeInvalidRequest = -32600
	// CodeMethodNotFound indicates that no handler is registered for a method.
	CodeMethodNotFound = -32601
	// CodeInvalidParams indicates invalid method parameters.
	CodeInvalidParams = -32602
	// CodeInternalError indicates an internal JSON-RPC failure.
	CodeInternalError = -32603
)
View Source
const Version = "2.0"

Version is the JSON-RPC protocol version implemented by this package.

Variables

View Source
var (
	// ErrTransport identifies a missing transport or a failure returned by it.
	ErrTransport = errors.New("jsonrpc: transport error")
	// ErrInvalidResponse identifies malformed envelopes or failed result decoding.
	ErrInvalidResponse = errors.New("jsonrpc: invalid response")
	// ErrMismatchedID identifies a response ID not matching a pending request.
	ErrMismatchedID = errors.New("jsonrpc: mismatched response id")
	// ErrUnexpectedResponse identifies notification replies or excess batch reply members.
	ErrUnexpectedResponse = errors.New("jsonrpc: unexpected response")
	// ErrMissingResponse identifies an incomplete set of batch responses.
	ErrMissingResponse = errors.New("jsonrpc: missing batch response")
	// ErrDuplicateResponse identifies repeated response IDs within a batch.
	ErrDuplicateResponse = errors.New("jsonrpc: duplicate batch response")
	// ErrDuplicateRequestID identifies ambiguous generated request IDs before batch transport.
	ErrDuplicateRequestID = errors.New("jsonrpc: duplicate batch request id")
	// ErrEmptyBatch identifies a client batch with no calls.
	ErrEmptyBatch = errors.New("jsonrpc: empty client batch")
	// ErrClientBatchTooLarge identifies a call count above the client batch bound.
	ErrClientBatchTooLarge = errors.New("jsonrpc: client batch too large")
	// ErrClientResponseTooLarge identifies a reply above the client parsing byte bound.
	ErrClientResponseTooLarge = errors.New("jsonrpc: client response too large")
)

Client errors identify transport, envelope, correlation, batch, and size failures and support errors.Is through direct return or wrapping.

View Source
var (
	// ErrHTTPStatus identifies a non-200, non-204 response after bounded body acquisition.
	ErrHTTPStatus = errors.New("jsonrpc: unexpected HTTP status")
	// ErrHTTPContentType identifies an unsupported content type on an HTTP 200 reply.
	ErrHTTPContentType = errors.New("jsonrpc: invalid HTTP response content type")
	// ErrResponseTooLarge identifies an HTTP reply body above the acquisition limit.
	ErrResponseTooLarge = errors.New("jsonrpc: HTTP response too large")
)

HTTP transport errors distinguish status, content-type, and body-limit failures and support errors.Is through direct return or wrapping.

View Source
var (
	// ErrInvalidMethodName identifies rpc. application registration or non-rpc. system registration.
	ErrInvalidMethodName = errors.New("jsonrpc: invalid method name")
	// ErrMethodAlreadyRegistered identifies an attempt to replace an existing registry name.
	ErrMethodAlreadyRegistered = errors.New("jsonrpc: method already registered")
	// ErrNilHandler identifies registration without a callable handler.
	ErrNilHandler = errors.New("jsonrpc: nil handler")
)

Registration errors identify reserved method names, duplicate methods, and nil handlers.

Functions

func Call

func Call[T any](ctx context.Context, client *Client, method string, params any) (T, error)

Call sends one request through a constructed, non-nil client and decodes T. On failure the returned value may contain partial decoding; consult the error.

func IsJSONContentType

func IsJSONContentType(value string) bool

IsJSONContentType reports whether value is application/json, application/json-rpc, or any successfully parsed media type ending in +json. Parameters are accepted; the suffix branch does not restrict the top-level type to application. JSONRPC-DEC-010 records the current binding policy.

Types

type AtomicIDGenerator

type AtomicIDGenerator struct {
	// contains filtered or unexported fields
}

AtomicIDGenerator generates concurrency-safe numeric IDs by int64 increments. Its zero value starts at 1. It must not be copied after use; arithmetic wraps at int64 overflow, so it is not an unlimited monotonic or unique-ID guarantee.

func NewAtomicIDGenerator

func NewAtomicIDGenerator(start int64) *AtomicIDGenerator

NewAtomicIDGenerator creates a generator whose first ID is start+1 using wrapping int64 arithmetic.

func (*AtomicIDGenerator) NextID

func (generator *AtomicIDGenerator) NextID() ID

NextID atomically increments the generator and returns the new numeric ID.

type BatchCall

type BatchCall struct {
	// Method is the JSON-RPC method name.
	Method string
	// Params must encode as an object or array when non-nil.
	Params any
	// Result is a caller-owned json.Unmarshal destination for success, when non-nil.
	Result any
	// Notification omits the request ID and expects no response member.
	Notification bool
	// Error receives a per-call protocol failure and is reset as the batch is built.
	// A nil Batch error does not mean every call succeeded; inspect each Error.
	Error *Error
	// contains filtered or unexported fields
}

BatchCall describes one member of a client batch. Batch writes a decoded success into Result and assigns a protocol failure to Error. These fields and internal correlation state are mutated; do not share calls or result targets concurrently. Earlier members may be updated even if Batch later returns an error.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client validates requests and correlates responses over a Transport. Its default AtomicIDGenerator is safe for concurrent calls; custom transports, generators, and BatchCall values retain their own concurrency contracts. Construct it with NewClient; the zero value is not ready for calls. The client borrows collaborators, performs no automatic retries and does not close them.

func NewClient

func NewClient(transport Transport, options ...ClientOption) *Client

NewClient constructs a client. A nil transport is reported as ErrTransport when an operation is attempted. Nil options are ignored.

func (*Client) Batch

func (client *Client) Batch(ctx context.Context, calls ...*BatchCall) error

Batch sends calls together and correlates every non-notification response by ID. It rejects empty batches, nil calls, duplicate generated IDs, and malformed response membership. Replies may arrive in any order. Valid per-call protocol failures populate BatchCall.Error without making Batch itself fail; envelope/transport/decoding failures return an error. Result mutation is not transactional and is not rolled back when a later reply fails validation.

Example
package main

import (
	"context"
	"fmt"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	transport := jsonrpc.TransportFunc(func(context.Context, []byte) ([]byte, error) {
		return []byte(`[
			{"jsonrpc":"2.0","result":4,"id":2},
			{"jsonrpc":"2.0","result":2,"id":1}
		]`), nil
	})
	client := jsonrpc.NewClient(transport)
	var first, second int
	calls := []*jsonrpc.BatchCall{
		{Method: "double", Params: []int{1}, Result: &first},
		{Method: "double", Params: []int{2}, Result: &second},
	}
	if err := client.Batch(context.Background(), calls...); err != nil {
		fmt.Println(err)
		return
	}
	for _, call := range calls {
		if call.Error != nil {
			fmt.Println(call.Error)
			return
		}
	}
	fmt.Println(first, second)
}
Output:
2 4

func (*Client) Call

func (client *Client) Call(ctx context.Context, method string, params, result any) error

Call sends one request, validates and correlates its response, and decodes a successful result into result when result is non-nil. The target must be a suitable json.Unmarshal destination and is caller-owned; decoding errors may leave it partially updated. A valid peer failure is returned as *Error; other failures preserve their category/causes. The transport enforces cancellation; Client does not preflight ctx.Err or impose a separate deadline.

func (*Client) Notify

func (client *Client) Notify(ctx context.Context, method string, params any) error

Notify sends a notification and requires an empty or whitespace-only reply. Success proves transport acceptance, not remote method completion. JSON-RPC notifications have no correlated outcome; callers own replay/observability.

type ClientOption

type ClientOption func(*Client)

ClientOption configures a Client during construction.

func WithIDGenerator

func WithIDGenerator(generator IDGenerator) ClientOption

WithIDGenerator borrows a non-nil request ID generator; nil leaves the existing generator unchanged. Typed-nil implementations are not filtered and must not be supplied. It does not validate future IDs in advance.

func WithMaxClientBatchItems added in v1.1.0

func WithMaxClientBatchItems(limit int) ClientOption

WithMaxClientBatchItems changes the default limit of 1,024 calls sent and response members inspected for one client batch. Non-positive values leave the existing bound unchanged.

func WithMaxClientResponseBytes

func WithMaxClientResponseBytes(limit int64) ClientOption

WithMaxClientResponseBytes changes the four-MiB parsing limit for positive values. Non-positive values leave it unchanged. Transport acquisition occurs before this check and needs its own bounds.

type Dispatcher

type Dispatcher struct {
	// contains filtered or unexported fields
}

Dispatcher validates and executes JSON-RPC requests and batches. Its configuration is immutable after construction; its Registry may be updated concurrently. Use NewDispatcher; the zero value is not ready for dispatch. It borrows the registry and callback state, which must be concurrency-safe. Options are construction-only; do not mutate configuration after publication.

func NewDispatcher

func NewDispatcher(registry *Registry, options ...DispatcherOption) *Dispatcher

NewDispatcher constructs a bounded dispatcher. A nil registry is replaced by an empty registry, and nil options are ignored.

func (*Dispatcher) Dispatch

func (d *Dispatcher) Dispatch(ctx context.Context, payload []byte) ([]byte, bool)

Dispatch processes one JSON-RPC message. The boolean reports whether the caller must send the returned response; notifications intentionally return no response for valid notifications, even on handler failure. Invalid id-less envelopes still return null-ID errors (JSONRPC-DEC-001); explicit null IDs are requests (JSONRPC-DEC-002). ctx must be nonnil; cancellation is propagated to callbacks, not preflight admission or forced preemption.

Payload is borrowed for this synchronous call; returned bytes are caller-owned. Batches currently execute sequentially, but ordering is not a public guarantee (JSONRPC-DEC-006). Aggregate response overflow still runs remaining handlers and returns one null-ID internal-error member, so side effects may complete without correlated outcomes (JSONRPC-DEC-011); do not blindly retry.

Notification and batch behavior follow:

func (*Dispatcher) DispatchSingle

func (d *Dispatcher) DispatchSingle(
	ctx context.Context,
	payload []byte,
) (Response, bool)

DispatchSingle processes one non-batch JSON-RPC message and returns its typed response before final wire encoding. The boolean is false for valid notifications, which return a zero Response; batches return Invalid Request. ctx/payload ownership and cancellation match Dispatch. Result shaping still encodes to enforce the response-size bound; this is not a zero-encoding API. The returned Response can retain a handler-owned Error/cause, not a deep copy.

type DispatcherOption

type DispatcherOption func(*Dispatcher)

DispatcherOption configures a Dispatcher during construction.

func WithErrorMapper

func WithErrorMapper(mapper ErrorMapper) DispatcherOption

WithErrorMapper replaces the default internal-error mapper. Returning nil or an invalid error is contained as an internal error.

func WithHooks

func WithHooks(hooks Hooks) DispatcherOption

WithHooks installs lifecycle observers. Hook panics are contained and hook response fields are copied, but OnResponse's request/cause are borrowed and must be treated as read-only to avoid changing shared state or wire IDs.

func WithMaxBatchItems

func WithMaxBatchItems(limit int) DispatcherOption

WithMaxBatchItems changes the dispatcher's default limit of 1,024 members. Non-positive values leave the existing bound unchanged.

func WithMaxDispatchBytes

func WithMaxDispatchBytes(limit int64) DispatcherOption

WithMaxDispatchBytes changes the dispatcher's four-MiB payload limit. Non-positive values leave the existing bound unchanged.

func WithMaxDispatchResponseBytes added in v1.1.0

func WithMaxDispatchResponseBytes(limit int64) DispatcherOption

WithMaxDispatchResponseBytes changes the dispatcher's four-MiB encoded response limit. Values smaller than the minimal protocol error use that minimal size so a valid bounded failure can still be returned. Non-positive values leave the limit unchanged. Encoding can allocate before this check; callers must bound handler values and custom marshaling (JSONRPC-DEC-011).

func WithMaxNestingDepth

func WithMaxNestingDepth(limit int) DispatcherOption

WithMaxNestingDepth changes the maximum number of nested JSON arrays and objects accepted before dispatch. The default matches encoding/json's built-in maximum depth of 10,000. Non-positive values leave the bound unchanged; raising it does not raise the standard JSON parser's own ceiling.

func WithMiddleware

func WithMiddleware(middleware ...Middleware) DispatcherOption

WithMiddleware appends function values in outermost-to-innermost order when the option runs. Captured state is not cloned; nil entries are skipped at execution.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"

	jsonrpc "github.com/faustbrian/go-jsonrpc"
)

func main() {
	registry := jsonrpc.NewRegistry()
	if err := registry.Register("ping", func(context.Context, json.RawMessage) (any, error) {
		return "pong", nil
	}); err != nil {
		fmt.Println(err)
		return
	}
	logging := func(next jsonrpc.Handler) jsonrpc.Handler {
		return func(ctx context.Context, params json.RawMessage) (any, error) {
			request, ok := jsonrpc.RequestFromContext(ctx)
			if !ok {
				return nil, jsonrpc.InternalError()
			}
			fmt.Println(request.Method)
			return next(ctx, params)
		}
	}
	dispatcher := jsonrpc.NewDispatcher(registry, jsonrpc.WithMiddleware(logging))
	response, reply := dispatcher.DispatchSingle(context.Background(), []byte(`{"jsonrpc":"2.0","method":"ping","id":1}`))
	if reply && response.Error != nil {
		fmt.Println(response.Error)
	}
}
Output:
ping

type Error

type Error struct {
	// Code is the integer JSON-RPC error code.
	Code int `json:"code"`
	// Message is the public error description.
	Message string `json:"message"`
	// Data contains optional public JSON details. Direct assignments borrow the
	// supplied slice; use WithData to encode a new owned buffer.
	Data json.RawMessage `json:"data,omitempty"`
	// contains filtered or unexported fields
}

Error is a mutable JSON-RPC error object. Message and Data are public wire content, not sanitized secrets. WithCause retains an unredacted local error exposed by Unwrap but excluded from JSON serialization. Do not share mutable errors across concurrent calls; even copies share Data and the cause. See https://www.jsonrpc.org/specification#error_object.

func DecodeParams

func DecodeParams[T any](params json.RawMessage) (T, *Error)

DecodeParams strictly decodes params as T. Duplicate or unknown named members, invalid UTF-8, malformed JSON and trailing data return InvalidParams. Exact top-level field names apply when T is a struct (including pointer forms); nested field case matching and custom unmarshaling retain encoding/json rules. Duplicate object names are rejected at every depth (JSONRPC-DEC-008). Direct callers must bound params before parsing. On error T may be partially decoded; use it only after checking the returned protocol error.

func InternalError

func InternalError() *Error

InternalError constructs the standard internal error.

func InvalidParams

func InvalidParams() *Error

InvalidParams constructs the standard invalid-params error.

func InvalidRequest

func InvalidRequest() *Error

InvalidRequest constructs the standard invalid-request error.

func MethodNotFound

func MethodNotFound() *Error

MethodNotFound constructs the standard method-not-found error.

func NewError

func NewError(code int, message string) *Error

NewError constructs a JSON-RPC error with code and public message. Application codes should avoid the protocol-reserved range -32768 through -32000. This constructor preserves code/message without enforcing that range or redacting them.

func ParseError

func ParseError() *Error

ParseError constructs the standard parse error.

func RequestLimitExceeded

func RequestLimitExceeded() *Error

RequestLimitExceeded constructs the dispatcher resource-limit error.

func (*Error) Error

func (e *Error) Error() string

Error returns a textual representation containing the public code and message.

func (*Error) UnmarshalJSON

func (e *Error) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes error fields, rejecting invalid UTF-8, duplicate envelope names and case variants of reserved names. Presence of code/message is checked later by Response.Validate; successful decoding alone is not validation. It clears old fields after preliminary decoding, but errors may leave partial receiver state. Use a separate value when rollback/alias isolation is needed.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the local cause, which is never serialized.

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

WithCause mutates and returns the receiver, borrowing cause without wire serialization. Local inspection/formatting of the cause remains caller-owned.

func (*Error) WithData

func (e *Error) WithData(value any) *Error

WithData JSON-encodes public error details. An encoding failure clears Data and is retained as the local cause. It mutates and returns the same receiver; successful encoding replaces Data but does not clear a previous cause.

type ErrorMapper

type ErrorMapper func(error) *Error

ErrorMapper converts an ordinary application error into public JSON-RPC data. It runs synchronously, may be invoked concurrently and owns the public message/ data disclosure policy. Dispatcher contains its panic or invalid/nil result; a directly returned or wrapped *Error bypasses this mapper.

type HTTPHandler

type HTTPHandler struct {
	// contains filtered or unexported fields
}

HTTPHandler adapts a Dispatcher to net/http with strict POST, content-type, and request-body handling. Construct it with NewHTTPHandler; the zero value is not usable. It borrows the dispatcher and owns no listener or server shutdown.

func NewHTTPHandler

func NewHTTPHandler(dispatcher *Dispatcher, options ...HTTPHandlerOption) *HTTPHandler

NewHTTPHandler constructs an HTTP handler. A nil dispatcher is replaced by an empty dispatcher, and nil options are ignored.

func (*HTTPHandler) ServeHTTP

func (h *HTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request)

ServeHTTP admits POST with an accepted JSON content type, then closes its bounded request body on every read/dispatch path. Earlier method/media rejection leaves body ownership to net/http or a direct caller. JSON-RPC replies use 200; valid notification-only calls use empty 204. Method, media, size and read failures use 405, 415, 413 and 400 (JSONRPC-DEC-009/010). Authentication, server deadlines and callback concurrency remain caller-owned; write/close errors are not returned.

type HTTPHandlerOption

type HTTPHandlerOption func(*HTTPHandler)

HTTPHandlerOption configures an HTTPHandler during construction.

func WithMaxRequestBytes

func WithMaxRequestBytes(limit int64) HTTPHandlerOption

WithMaxRequestBytes changes the four-MiB body limit for positive values; non-positive values leave it unchanged. Dispatcher bounds are independent.

type HTTPStatusError

type HTTPStatusError struct {
	// StatusCode is the peer's HTTP response status.
	StatusCode int
	// Body is an untrusted, control-character-sanitized preview bounded to four
	// KiB. It is empty by default, and Error deliberately omits it.
	Body string
}

HTTPStatusError reports a non-200 HTTP response. Body is empty by default and populated only when the transport enables a bounded diagnostic preview.

func (*HTTPStatusError) Error

func (err *HTTPStatusError) Error() string

Error returns only the status code. Body is excluded because it is controlled by the remote peer and may contain secrets.

func (*HTTPStatusError) Unwrap

func (err *HTTPStatusError) Unwrap() error

Unwrap returns ErrHTTPStatus.

type HTTPTransport

type HTTPTransport struct {
	// contains filtered or unexported fields
}

HTTPTransport exchanges JSON-RPC payloads over HTTP POST. Construct it with NewHTTPTransport; the zero value is not usable. Concurrent RoundTrip calls are supported with a concurrency-safe borrowed client and construction-only options. It starts no owned background worker and does not close client idle connections.

func NewHTTPTransport

func NewHTTPTransport(endpoint string, options ...HTTPTransportOption) (*HTTPTransport, error)

NewHTTPTransport validates an HTTP(S) endpoint without URL user information and constructs a transport. The default client has a 30-second timeout and does not follow redirects or use environment proxies. Nil options are ignored. HTTP(S), loopback/private destinations and query strings are allowed; endpoint validation is not SSRF protection. Callers own origin/DNS/TLS/egress policy.

func (*HTTPTransport) RoundTrip

func (transport *HTTPTransport) RoundTrip(ctx context.Context, payload []byte) ([]byte, error)

RoundTrip posts payload and returns a bounded JSON response. A 204 response returns nil without reading/validating a JSON body (JSONRPC-DEC-009); other non-200 statuses return *HTTPStatusError after bounded body acquisition. Size or read failures can precede status classification. A 200 response needs an accepted content type, but envelope validation belongs to Client. Payload is borrowed until return and returned body bytes are caller-owned. Every received body is closed. Context cancellation and the configured HTTP client bound network work; no retry or extra deadline is imposed here.

type HTTPTransportOption

type HTTPTransportOption func(*HTTPTransport)

HTTPTransportOption configures an HTTPTransport during construction.

func WithHTTPClient

func WithHTTPClient(client *http.Client) HTTPTransportOption

WithHTTPClient installs a non-nil HTTP client. Its timeout, redirect, proxy, DNS, and dial policies remain caller-owned. The client is borrowed, not cloned or closed; nil leaves the existing client unchanged.

func WithHTTPDiagnosticPreviewBytes added in v1.1.0

func WithHTTPDiagnosticPreviewBytes(limit int64) HTTPTransportOption

WithHTTPDiagnosticPreviewBytes opts into retaining up to limit bytes of a non-success response body in HTTPStatusError.Body. Values above four KiB are clamped to four KiB. Non-positive values leave an existing preview setting unchanged. Control sanitation is not secret redaction; Error never prints Body.

func WithHTTPHeader

func WithHTTPHeader(name, value string) HTTPTransportOption

WithHTTPHeader adds a header to each request. Content-Type and Accept are always overwritten with JSON values during RoundTrip.

func WithMaxResponseBytes

func WithMaxResponseBytes(limit int64) HTTPTransportOption

WithMaxResponseBytes changes the four-MiB HTTP acquisition limit for positive values; non-positive values leave it unchanged. Client parsing has its own limit.

type Handler

type Handler func(context.Context, json.RawMessage) (any, error)

Handler implements one JSON-RPC method synchronously. Params is borrowed for the invocation; copy bytes before retaining or mutating them. Handlers own cooperative cancellation, authorization, replay safety and bounded returned values/custom marshaling. Concurrent dispatch may invoke a handler concurrently.

type Hooks

type Hooks struct {
	// OnRequest observes a copied validated request or nil for an invalid
	// envelope and may return a derived context for subsequent processing.
	// Params/ID raw bytes are copied for this callback. Nil or panic preserves ctx;
	// any other returned context is used without enforcing the original deadline.
	// Derive it from ctx to preserve cancellation and caller context values.
	OnRequest func(context.Context, *Request) context.Context
	// OnResponse observes copied response fields/raw buffers, including notification
	// outcomes, before final wire encoding. Request is borrowed, not copied: treat
	// it and its ID/Params storage as read-only. Mutating/re-decoding Request.ID can
	// alter shared response ID bytes. A local Error cause also remains borrowed
	// and may carry sensitive error or panic-stack data. Copy before retention.
	// Observed outcomes may differ from final aggregate/encoding-limit fallbacks.
	OnResponse func(context.Context, *Request, *Response)
}

Hooks observe the complete dispatcher lifecycle, including protocol errors that occur before a Handler or Middleware can run. A nil Request represents an unparseable or invalid request. Notifications provide an internal outcome to OnResponse even though that response is never placed on the wire. Callbacks run synchronously without registry locks and may run concurrently across dispatches. Panics are contained, not blocking or unbounded work.

type ID

type ID struct {
	// contains filtered or unexported fields
}

ID preserves string, number or null wire spelling separately from correlation identity. The zero value is missing, not explicit null. Copies share raw storage: re-decoding a copied ID can change an alias's wire spelling, so treat IDs borrowed from requests/context/hooks as read-only and decode into a fresh value when independent ownership is needed. See https://www.jsonrpc.org/specification#request_object.

func NullID

func NullID() ID

NullID constructs explicit null, distinct from the missing zero-value ID. JSONRPC-DEC-002 treats a present null request ID as requiring a response.

func NumberID

func NumberID(value json.Number) ID

NumberID preserves value's wire spelling and exact-decimal correlation key. It does not validate that value is valid JSON numeric syntax. NewRequest rejects invalid IDs; direct users must supply valid numbers. JSONRPC-DEC-003 records equivalence without binary floating-point conversion or exponent expansion.

func StringID

func StringID(value string) ID

StringID constructs a string ID. Invalid UTF-8 is replaced according to encoding/json rules so correlation matches the transmitted value.

func (ID) Equal

func (id ID) Equal(other ID) bool

Equal reports whether IDs have the same kind and value. Mathematically equivalent valid numeric spellings compare equal; strings and numbers never coerce to one another. Equal does not validate IDs.

func (ID) Kind

func (id ID) Kind() IDKind

Kind returns the ID's representation kind.

func (ID) MarshalJSON

func (id ID) MarshalJSON() ([]byte, error)

MarshalJSON preserves the ID's original JSON spelling. A missing ID marshals as null when encoded outside a Request. Returned bytes are an independent copy; this method does not validate raw numeric syntax.

func (*ID) UnmarshalJSON

func (id *ID) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a string, number, or null ID without numeric precision loss and rejects invalid UTF-8, non-ID types and trailing values. It copies input bytes but may reuse receiver storage shared by prior ID value copies. Standalone callers must bound input size; no dispatcher byte limit applies here.

type IDGenerator

type IDGenerator interface {
	// NextID returns a valid non-missing ID for correlation. Client calls may invoke
	// it concurrently; a custom implementation must synchronize its own state.
	// Duplicate generated IDs within a batch are rejected before transport.
	NextID() ID
}

IDGenerator supplies request IDs to a Client.

type IDKind

type IDKind uint8

IDKind identifies the JSON representation of an ID.

const (
	// IDMissing represents an absent ID member and therefore a notification.
	IDMissing IDKind = iota
	// IDString represents a JSON string ID.
	IDString
	// IDNumber represents a JSON number ID.
	IDNumber
	// IDNull represents an explicit JSON null ID.
	IDNull
)

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler with cross-cutting behavior. Its constructor runs for each method execution, outside registry locks, in reverse wrapping order. Both it and its returned handler must support the caller's concurrency; neither gets automatic timeouts or bounded recursion from Dispatcher.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a concurrency-safe method registry. Its zero value is ready for use. A Registry must not be copied after first use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry constructs an empty registry.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Handler, bool)

Lookup returns the borrowed handler under name after releasing the registry read lock. The caller owns its invocation and any captured mutable state.

func (*Registry) Register

func (r *Registry) Register(name string, handler Handler) error

Register adds handler under name. Names beginning with rpc. are reserved, and an existing name cannot be replaced. Empty application names are valid. The registry borrows the function/captured state; it does not run the handler under its lock or provide synchronization for that state.

func (*Registry) RegisterSystem

func (r *Registry) RegisterSystem(name string, handler Handler) error

RegisterSystem adds an explicitly reserved rpc.* handler. This method is intended for protocol extensions such as OpenRPC's rpc.discover and rejects application method names.

type Request

type Request struct {
	// JSONRPC must equal Version.
	JSONRPC string `json:"jsonrpc"`
	// Method is the case-sensitive method name.
	Method string `json:"method"`
	// Params contains an optional JSON object or array; literal assignments borrow
	// the slice, while the JSON decoder and builders allocate their own bytes.
	Params json.RawMessage `json:"params,omitempty"`
	// ID identifies a request; it is missing for notifications.
	ID ID `json:"-"`
	// contains filtered or unexported fields
}

Request represents the protocol's request object. Params, when present, must follow https://www.jsonrpc.org/specification#parameter_structures. UnmarshalJSON records presence; Validate separately checks the envelope. Value copies share Params and ID raw storage; mutation requires caller ownership. See https://www.jsonrpc.org/specification#request_object.

func NewNotification

func NewNotification(method string, params any) (Request, error)

NewNotification validates method/params as NewRequest does, omits the ID member and therefore cannot receive a correlated protocol response.

func NewRequest

func NewRequest(method string, params any, id ID) (Request, error)

NewRequest validates a non-missing ID and rejects reserved rpc. application method names (empty names are permitted). Nil params omit the member; nonnil params must JSON-encode as an object or array. Encoding allocates owned Params; the supplied ID is a value copy sharing its raw storage. Explicit NullID is accepted as a request per JSONRPC-DEC-002, not treated as a notification.

func RequestFromContext

func RequestFromContext(ctx context.Context) (Request, bool)

RequestFromContext returns the validated request made available to the active middleware or handler. ctx must be nonnil. The returned value is a shallow copy sharing Params and ID storage; treat it as read-only and create independent copies before retention or mutation.

func (Request) IsNotification

func (r Request) IsNotification() bool

IsNotification reports whether the request omitted its ID member as defined by https://www.jsonrpc.org/specification#notification.

func (Request) MarshalJSON

func (r Request) MarshalJSON() ([]byte, error)

MarshalJSON encodes fields without calling Validate, preserving notification ID omission. Invalid raw Params/ID can produce an encoding error.

func (*Request) UnmarshalJSON

func (r *Request) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a request while preserving whether ID and method were present and rejecting invalid UTF-8, duplicate envelope names and case variants of reserved names. Unknown extension names are allowed. Nested application data keeps ordinary JSON behavior unless DecodeParams is used. Call Validate after success; an error may leave the receiver partially changed.

func (Request) Validate

func (r Request) Validate() *Error

Validate returns an InvalidRequest error when the request envelope violates JSON-RPC 2.0. This checks version, method presence and params shape, not reserved-method registration policy or validity of a programmatically supplied ID. NewRequest and UnmarshalJSON validate IDs at their respective boundaries.

type Response

type Response struct {
	// JSONRPC must equal Version.
	JSONRPC string `json:"jsonrpc"`
	// Result contains raw success JSON, including an explicit null. Literal
	// assignments borrow the slice; mutating it requires caller ownership.
	Result json.RawMessage `json:"result,omitempty"`
	// Error contains the failure object; pointer copies retain that mutable object.
	Error *Error `json:"error,omitempty"`
	// ID correlates the response with its request.
	ID ID `json:"id"`
	// contains filtered or unexported fields
}

Response represents the object defined by https://www.jsonrpc.org/specification#response_object. Result, Error.Data and ID raw buffers can be shared by copies. Hidden presence state is set by decoding/dispatch; exported fields in a literal alone do not satisfy Validate.

func (Response) MarshalJSON

func (r Response) MarshalJSON() ([]byte, error)

MarshalJSON selects error when Error is nonnil or decoded error presence is set; otherwise it encodes result, using null when Result is nil. It does not call Validate or preserve an invalid result/error conflict.

func (*Response) UnmarshalJSON

func (r *Response) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a response and records the presence of result, error, and ID members for later validation. It rejects invalid UTF-8, duplicate/case variants of reserved envelope names and malformed field representations, but successful decoding does not guarantee a valid envelope. Errors may leave partial receiver state; Validate checks completeness and exclusivity.

func (Response) Validate

func (r Response) Validate() error

Validate checks decoded/dispatcher presence state, version, non-missing ID, result/error exclusivity and error shape. It is not a validator for arbitrary Response literals whose hidden presence flags were never set.

type Transport

type Transport interface {
	// RoundTrip synchronously exchanges payload using the supplied context.
	// Implementations own cancellation and acquisition bounds; Client checks reply
	// size only after return. Treat payload as borrowed until return, and return
	// reply bytes safe for the caller to inspect without concurrent mutation.
	RoundTrip(context.Context, []byte) ([]byte, error)
}

Transport exchanges one complete JSON-RPC payload with a peer.

type TransportFunc

type TransportFunc func(context.Context, []byte) ([]byte, error)

TransportFunc adapts a function to Transport.

func (TransportFunc) RoundTrip

func (function TransportFunc) RoundTrip(ctx context.Context, payload []byte) ([]byte, error)

RoundTrip calls function with ctx and payload.

Directories

Path Synopsis
cmd
semvercheck command
Package main provides the semvercheck release-tag validation and bump command.
Package main provides the semvercheck release-tag validation and bump command.
examples
client command
Package main demonstrates a JSON-RPC call to the separately run example server.
Package main demonstrates a JSON-RPC call to the separately run example server.
e2e command
Package main demonstrates an owned in-process HTTP server and JSON-RPC client.
Package main demonstrates an owned in-process HTTP server and JSON-RPC client.
server command
Package main demonstrates a JSON-RPC HTTP server with caller-owned timeouts.
Package main demonstrates a JSON-RPC HTTP server with caller-owned timeouts.
internal
semver
Package semver implements repository-private release-tag syntax and bump helpers.
Package semver implements repository-private release-tag syntax and bump helpers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL