jsonrpc

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 15 Imported by: 0

README

jsonrpc

A small, transport-agnostic JSON-RPC 2.0 library for Go.

The core is a Server that dispatches decoded requests to registered handlers. The same dispatch logic backs every transport, so an identical set of methods can be served over HTTP and over a byte stream (stdin/stdout) without change. A matching Client talks to a remote endpoint over HTTP, an arbitrary stream, or a spawned subprocess.

  • Full JSON-RPC 2.0: requests, notifications, batches, and the standard error codes.
  • Request ids round-trip byte-for-byte (number, string, or null shape preserved).
  • Params are handed to handlers as raw JSON, so integer precision and custom decoding are entirely under your control.
  • Concurrent dispatch by default, with serialised writes.
  • No third-party dependencies.
go get github.com/paularlott/jsonrpc

Requires the Go version declared in go.mod. The typed-handler helper uses generics (Go 1.18+).

Server

A Server is a method registry. Register handlers, then serve them over any transport.

srv := jsonrpc.NewServer()

// Raw handler: decode params yourself.
srv.Handle("echo", func(ctx context.Context, params json.RawMessage) (any, error) {
    var v any
    if err := jsonrpc.Unmarshal(params, &v); err != nil {
        return nil, err // becomes an Invalid Params error
    }
    return v, nil
})

// Typed handler: params decoded into T for you.
srv.Handle("add", jsonrpc.Typed(func(ctx context.Context, p struct {
    A, B int
}) (any, error) {
    return p.A + p.B, nil
}))
Over HTTP

Server implements http.Handler. Mount it anywhere:

http.Handle("/rpc", srv)
http.ListenAndServe(":8080", nil)

It accepts POST with a single request object or a batch array. A message that produces no response (a notification, or an all-notification batch) returns 204 No Content; otherwise the JSON response is returned with 200 OK.

Over a stream (stdio)

ServeStream runs a newline-delimited JSON-RPC loop, the shape used for stdio servers (including MCP stdio servers):

srv.ServeStream(context.Background(), os.Stdin, os.Stdout)

It reads JSON values from the reader (they need not be newline-separated) and writes each response as one line to the writer. It blocks until the reader reaches EOF. Keep stdout clean — write logs to stderr.

Embedding in another transport

HandleMessage(ctx, raw) ([]byte, bool) dispatches one raw message (single or batch) and returns the marshalled response and whether a response is due. HandleRequest(ctx, *Request) *Response dispatches a single decoded request (returning nil for notifications). Use these to wire the dispatcher into a transport the library doesn't provide.

Client

A Client wraps a Transport, generating ids and encoding params for you.

client := jsonrpc.NewClient(jsonrpc.NewHTTPTransport("http://localhost:8080/rpc"))
defer client.Close()

var sum int
err := client.Call(ctx, "add", map[string]int{"A": 2, "B": 40}, &sum) // sum == 42

// Notifications expect no response.
err = client.Notify(ctx, "ping", nil)

A JSON-RPC error response is returned as a *jsonrpc.Error:

err := client.Call(ctx, "div", pair{A: 1, B: 0}, nil)
var rpcErr *jsonrpc.Error
if errors.As(err, &rpcErr) {
    fmt.Println(rpcErr.Code, rpcErr.Message, rpcErr.Data)
}
Batching

CallBatch sends several calls as one JSON-RPC 2.0 batch request instead of one round-trip per call:

var sum, product int
results := client.CallBatch(ctx, []jsonrpc.BatchCall{
    {Method: "add", Params: pair{A: 2, B: 40}, Out: &sum},
    {Method: "mul", Params: pair{A: 6, B: 7}, Out: &product},
})
for i, r := range results {
    if r.Err != nil {
        fmt.Println("call", i, "failed:", r.Err)
    }
}

Results are always returned in call order, regardless of transport or response ordering, and one call's error (transport failure or JSON-RPC error response, returned as *jsonrpc.Error) does not affect the others. HTTPTransport and StreamTransport both send the batch as a single wire message (one HTTP POST, or one JSON array line); other Transport implementations still work via a concurrent per-call fallback, so CallBatch is always safe to use.

Transports
Transport Constructor Use
HTTP NewHTTPTransport(url, ...) Talk to an HTTP JSON-RPC endpoint. Options: WithHTTPClient, WithHeader.
Stream NewStreamTransport(in, out, ...) Newline-delimited JSON over any reader/writer (pipes, sockets).
Subprocess NewProcessTransport(name, args, ...) Spawn a command and speak JSON-RPC over its stdin/stdout. Options: WithStderr, WithShutdownTimeout, WithEnv, WithExtraEnv, WithDir, WithOnExit.

The stream and subprocess transports run a background reader that correlates responses to calls by id, so many calls and notifications can be in flight concurrently.

Subprocess lifecycle

NewProcessTransport reaps the child in a background goroutine the moment it exits — whether it crashes mid-session or is shut down by Close — so it never becomes a zombie. Pass WithOnExit to be told when that happens:

transport, err := jsonrpc.NewProcessTransport(serverCmd, args,
    jsonrpc.WithOnExit(func(err error) {
        log.Printf("server exited: %v", err) // nil for a clean exit
        // unregister, restart, etc.
    }),
)

The callback fires exactly once, asynchronously, after the child has been reaped; it must not block, and Close does not wait for it. Without it a caller only learns a spawned server has died when subsequent calls start failing.

WithEnv and WithExtraEnv control the child's environment. WithEnv replaces the whole environment (os/exec Cmd.Env semantics — every variable, including PATH/HOME, must be listed). WithExtraEnv merges KEY=VALUE entries on top of the inherited parent environment, so it is the right choice for setting a few variables while keeping the rest intact; later values win for a repeated key.

// Keep the parent environment; add/override a couple of variables.
transport, _ := jsonrpc.NewProcessTransport(serverCmd, args,
    jsonrpc.WithExtraEnv("LOG_LEVEL=debug", "DATA_DIR=/var/data"),
)

Peer (bidirectional)

A Client only sends requests; a Server only handles them. A Peer is a Client and a Server sharing one connection, so each end can both call out and serve calls in. Use it when the two sides talk back and forth — for example a host that calls a plugin's methods while the plugin calls host "callback" methods back (remote objects, progress, elicitation). Both ends are Peers: whoever initiates a call is acting as the Client for that call, and whoever receives it is acting as the Server.

The two ends connect with a bidirectional stream — for a subprocess that is the child's stdin/stdout; in-process it would be a pair of io.Pipes or a socket. Each end is a Peer: whoever initiates a call is acting as the Client, whoever receives it is acting as the Server. The runnable programs live in examples/peer-plugin and examples/peer-client; both are shown in full below.

Plugin example

examples/peer-plugin serves greet and, while handling it, calls progress back on the host. It is a Peer over stdin/stdout: NewPeer builds it from its inbound server, and peer.Client() is its outbound side. Construction order matters — NewPeer does not start the reader, so the greet handler is registered after the peer is created and before Serve runs it (the handler closes over peer, which is assigned by then).

// Command peer-plugin is the server side of a bidirectional JSON-RPC 2.0
// connection. It serves "greet" and, mid-call, calls "progress" back on its
// caller — the same stdin/stdout stream carries both directions.
package main

import (
	"context"
	"encoding/json"
	"log"
	"os"

	"github.com/paularlott/jsonrpc"
)

func main() {
	log.SetOutput(os.Stderr) // stdout is the protocol stream; logs go to stderr

	srv := jsonrpc.NewServer()
	// A Peer = a Server (inbound) + a Client (outbound) over one stream.
	peer := jsonrpc.NewPeer(os.Stdin, os.Stdout, srv)

	// Inbound: the plugin is the Server for "greet".
	srv.Handle("greet", func(ctx context.Context, params json.RawMessage) (any, error) {
		var name string
		if err := jsonrpc.Unmarshal(params, &name); err != nil {
			return nil, err
		}
		// Outbound: now the plugin is the Client, calling a method the host
		// serves. This reverse-direction call rides the same connection.
		var ack string
		if err := peer.Client().Call(ctx, "progress", "about to greet "+name, &ack); err != nil {
			return nil, err
		}
		log.Printf("host acknowledged progress: %s", ack)
		return "Hello, " + name + "!", nil
	})

	if err := peer.Serve(); err != nil { // blocks until stdin closes
		log.Fatalf("serve: %v", err)
	}
}
Client example

examples/peer-client spawns the plugin and is itself a Peer: it is the Client for greet and the Server for the progress callback. The packages used are just context, encoding/json, and jsonrpc. Setup is: register the callback handler on a server, then NewProcessPeer spawns the plugin with that server (and starts the reader automatically). The call to the plugin and the callback it triggers share the one connection.

// Command peer-client spawns peer-plugin, calls its "greet" method, and serves
// the "progress" callback the plugin invokes mid-call.
package main

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

	"github.com/paularlott/jsonrpc"
)

func main() {
	name := "go"
	args := []string{"run", "github.com/paularlott/jsonrpc/examples/peer-plugin"}
	if len(os.Args) > 1 {
		name = os.Args[1]
		args = os.Args[2:]
	}

	// Inbound: the host is the Server for the plugin's callbacks.
	hostSrv := jsonrpc.NewServer()
	hostSrv.Handle("progress", func(ctx context.Context, params json.RawMessage) (any, error) {
		var msg string
		_ = jsonrpc.Unmarshal(params, &msg)
		fmt.Printf("[callback] plugin: %s\n", msg)
		return "ok", nil // the plugin (acting as client) receives this result
	})

	// Spawn the plugin as a bidirectional peer. NewProcessPeer starts the
	// reader itself, so the returned peer can both call and serve immediately.
	peer, err := jsonrpc.NewProcessPeer(name, args, hostSrv, jsonrpc.WithStderr(os.Stderr))
	if err != nil {
		log.Fatalf("spawn plugin: %v", err)
	}
	defer peer.Close()

	// Outbound: the host is the Client for "greet". While the plugin handles
	// this, it calls "progress" back on us (served by hostSrv above).
	var greeting string
	if err := peer.Client().Call(context.Background(), "greet", "World", &greeting); err != nil {
		log.Fatalf("greet: %v", err)
	}
	fmt.Println(greeting)
}

Build the plugin and run the client against it:

go build -o /tmp/peer-plugin ./examples/peer-plugin
go run ./examples/peer-client /tmp/peer-plugin

The callback fires mid-greet (the plugin is acting as a client to the host), then the greeting is returned:

[callback] plugin: about to greet World
host acknowledged progress: ok
Hello, World!

How it fits together:

  • peer.Client() is the outbound side — a real *Client (generates ids, correlates responses). peer.Handle(...) is the inbound side — it delegates to the *Server passed to NewPeer.
  • The reader demultiplexes each incoming message: a response (no method) goes to the pending outbound Client.Call; a request (has method) is dispatched to the Server, and its response is written back. So the same bytes carry both directions, classified by the presence of method.
  • Inbound handlers run in their own goroutine, so a slow handler — or one that makes a nested reverse-direction call (like do_work calling progress) cannot stall delivery of outbound responses.
  • NewPeer does not start the reader; call Serve from the goroutine that owns the input. That lets a caller wire up state its handlers depend on before any handler can run. Serve blocks until EOF; follow it with peer.Wait() to block until in-flight handlers have flushed. Peer.Close cancels in-flight handler contexts and runs a close function given via WithPeerCloseFunc.

For a spawned subprocess peer, use NewProcessPeer(name, args, server, opts...) — the bidirectional counterpart to NewProcessTransport. It accepts the same process options (WithStderr, WithEnv, WithExtraEnv, WithDir, WithOnExit, WithShutdownTimeout) and closing the Peer drives the child down. Unlike NewPeer, NewProcessPeer starts the background reader itself, so the returned Peer is ready to call immediately.

Errors

Return a *jsonrpc.Error from a handler to control the wire error exactly; any other non-nil error becomes a server error (-32000). A handler panic is recovered and reported as an internal error (-32603).

return nil, jsonrpc.NewError(jsonrpc.CodeInvalidParams, "division by zero", map[string]string{"field": "b"})

Standard codes are exported as CodeParseError, CodeInvalidRequest, CodeMethodNotFound, CodeInvalidParams, CodeInternalError and CodeServerError, with matching constructor helpers (ParseError, InvalidRequest, …).

Notifications

A notification is a request without an id; the same handler serves both request and notification calls, and the return value is discarded for notifications. An unknown notification is silently ignored (per spec), while an unknown request returns a Method Not Found error. To observe errors from notification handlers, pass WithErrorHandler.

Call jsonrpc.IsNotification(ctx) inside a handler to tell the two call shapes apart — for example to reject a method-only handler when it's invoked as a notification, or vice versa:

srv.Handle("event", func(ctx context.Context, params json.RawMessage) (any, error) {
    if !jsonrpc.IsNotification(ctx) {
        return nil, jsonrpc.MethodNotFound("event is notification-only")
    }
    // ... handle the fire-and-forget call
    return nil, nil
})

Concurrency

By default the server dispatches each incoming message — and each element of a batch — concurrently, serialising only the writes. Pass WithSerialDispatch to process messages one at a time in arrival order. The library is verified with go test -race and a fuzz target over HandleMessage.

Examples

Runnable programs live in examples/:

License

MIT — see LICENSE.txt.

Documentation

Overview

Package jsonrpc implements the JSON-RPC 2.0 specification with pluggable transports.

The package is transport-agnostic at its core: a Server dispatches decoded requests to registered handlers, and the same dispatch logic backs the HTTP handler (Server.ServeHTTP) and the newline-delimited stream loop (Server.ServeStream, suitable for stdio). A Client talks to a remote endpoint over any Transport — HTTP (HTTPTransport), an arbitrary byte stream (StreamTransport), or a spawned subprocess (NewProcessTransport). When both ends must act as client and server on a single stream, a Peer combines the two (NewPeer, or NewProcessPeer for a subprocess).

The wire format follows JSON-RPC 2.0 exactly: request/notification framing, batch arrays, notification (no id) semantics, and the standard error codes. Request ids are preserved as raw JSON so their original shape (number, string or null) round-trips into the response unchanged, and request params are kept as encoding/json.RawMessage so handlers control their own decoding (preserving integer precision, custom number handling, etc.).

Index

Constants

View Source
const (
	// CodeParseError indicates invalid JSON was received by the server.
	CodeParseError = -32700
	// CodeInvalidRequest indicates the JSON sent is not a valid Request object.
	CodeInvalidRequest = -32600
	// CodeMethodNotFound indicates the method does not exist or is not available.
	CodeMethodNotFound = -32601
	// CodeInvalidParams indicates invalid method parameters.
	CodeInvalidParams = -32602
	// CodeInternalError indicates an internal JSON-RPC error.
	CodeInternalError = -32603
	// CodeServerError is the conventional code for an application/server error
	// (reserved range -32000 to -32099).
	CodeServerError = -32000
)

Standard JSON-RPC 2.0 error codes.

Codes from -32000 to -32099 are reserved for implementation-defined server errors; CodeServerError is the conventional default within that range.

View Source
const Version = "2.0"

Version is the JSON-RPC protocol version emitted on every response and required (when present) on every request.

Variables

View Source
var ErrClosed = errors.New("jsonrpc: transport closed")

ErrClosed is returned by a stream transport when a call cannot complete because the transport has been closed or its underlying stream ended.

Functions

func IsNotification

func IsNotification(ctx context.Context) bool

IsNotification reports whether the in-flight call is a JSON-RPC notification (a request without an id, for which no response is sent). It is valid only inside a Handler; it returns false elsewhere.

The same handler serves both requests and notifications, so this lets a handler adjust its behaviour for the fire-and-forget case — for example to reject notification-only or request-only methods.

func Unmarshal

func Unmarshal(params json.RawMessage, v any) error

Unmarshal decodes raw params into v. It is a thin convenience wrapper over json.Unmarshal that treats absent (empty) params as a no-op, leaving v at its zero value. Returns a CodeInvalidParams Error on failure so it can be returned directly from a Handler.

Types

type BatchCall

type BatchCall struct {
	Method string
	Params any
	Out    any
}

BatchCall is a single call within a batch request to Client.CallBatch. Out mirrors the out parameter of Client.Call: nil discards the result, otherwise the result is JSON-decoded into it. Out is only touched from CallBatch itself (not concurrently), so it need not be safe for concurrent use.

type BatchResult

type BatchResult struct {
	Result json.RawMessage
	Err    error
}

BatchResult is the outcome of one BatchCall from Client.CallBatch. Err is non-nil on a transport failure or a JSON-RPC error response (in which case it is a *Error); Result holds the raw result bytes for callers that did not set Out, or who want to inspect the wire value directly.

type Client

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

Client is a JSON-RPC 2.0 client. It is a thin, transport-agnostic wrapper that generates request ids, encodes params, and decodes results. It is safe for concurrent use if the underlying Transport is.

func NewClient

func NewClient(t Transport) *Client

NewClient wraps a Transport in a client.

func (*Client) Call

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

Call invokes method with params and decodes the result into out.

params is encoded to JSON (a nil params sends no params member; a json.RawMessage is sent verbatim). out may be nil to discard the result; otherwise the result is JSON-decoded into out. A JSON-RPC error response is returned as a *Error.

func (*Client) CallBatch

func (c *Client) CallBatch(ctx context.Context, calls []BatchCall) []BatchResult

CallBatch sends every call in a single JSON-RPC 2.0 batch request when the underlying Transport supports it (HTTPTransport and StreamTransport both do), cutting N round-trips down to one. Transports without native batch support still work: CallBatch falls back to issuing the calls concurrently over Transport.Call.

Results are returned in the same order as calls, regardless of transport or response ordering. A per-call failure (transport error or JSON-RPC error response) is reported in that call's BatchResult.Err; it does not affect other calls in the batch. An empty calls slice returns an empty, non-nil slice.

func (*Client) Close

func (c *Client) Close() error

Close closes the underlying transport.

func (*Client) Notify

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

Notify sends a notification: a method call for which no response is expected.

type Error

type Error struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    any    `json:"data,omitempty"`
}

Error is a JSON-RPC 2.0 error object. It implements the error interface so handlers can return it directly and clients receive it from Client.Call.

Data is optional and, when non-nil, is serialised into the "data" member of the error object.

func Errorf

func Errorf(code int, format string, args ...any) *Error

Errorf builds an Error with a formatted message and no data.

func InternalError

func InternalError(message string) *Error

InternalError returns a -32603 error with the given message.

func InvalidParams

func InvalidParams(message string) *Error

InvalidParams returns a -32602 error with the given message.

func InvalidRequest

func InvalidRequest(message string) *Error

InvalidRequest returns a -32600 error with the given message.

func MethodNotFound

func MethodNotFound(message string) *Error

MethodNotFound returns a -32601 error with the given message.

func NewError

func NewError(code int, message string, data any) *Error

NewError builds an Error with the given code, message and optional data.

func ParseError

func ParseError(message string) *Error

ParseError returns a -32700 error with the given message.

func ServerError

func ServerError(message string) *Error

ServerError returns a -32000 error with the given message.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type HTTPOption

type HTTPOption func(*HTTPTransport)

HTTPOption configures an HTTPTransport.

func WithHTTPClient

func WithHTTPClient(client *http.Client) HTTPOption

WithHTTPClient sets the http.Client used for requests. By default http.DefaultClient is used.

func WithHeader

func WithHeader(key, value string) HTTPOption

WithHeader adds a header sent on every request (e.g. Authorization). It may be called multiple times to add several headers.

type HTTPTransport

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

HTTPTransport is a Transport that sends each call as an HTTP POST carrying the JSON-RPC request in the body. It is safe for concurrent use.

func NewHTTPTransport

func NewHTTPTransport(url string, opts ...HTTPOption) *HTTPTransport

NewHTTPTransport creates an HTTP transport targeting url.

func (*HTTPTransport) Call

func (t *HTTPTransport) Call(ctx context.Context, req *Request) (*Response, error)

Call implements Transport.

func (*HTTPTransport) CallBatch

func (t *HTTPTransport) CallBatch(ctx context.Context, reqs []*Request) ([]*Response, error)

CallBatch implements the batch-capable transport extension used by Client.CallBatch: it POSTs the whole batch as a single JSON-RPC array and decodes the array response.

func (*HTTPTransport) Close

func (t *HTTPTransport) Close() error

Close implements Transport. It is a no-op for HTTP.

func (*HTTPTransport) Notify

func (t *HTTPTransport) Notify(ctx context.Context, req *Request) error

Notify implements Transport. The response body (if any) is drained and discarded; a 2xx status is treated as success.

type Handler

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

Handler processes a single JSON-RPC method call.

params carries the raw JSON of the request's params member (nil when absent), letting the handler decode into whatever type it wants — use Unmarshal or json.Unmarshal directly. The returned value is marshalled into the response result. To signal a JSON-RPC error, return a *Error (its code, message and data are used verbatim); any other non-nil error becomes a CodeServerError.

The same handler serves both requests and notifications. When invoked for a notification the return value and error are discarded (no response is sent), though a returned error is still reported to an [Options.OnError] hook if set.

func Typed

func Typed[T any](fn func(ctx context.Context, params T) (any, error)) Handler

Typed adapts a strongly-typed handler into a Handler. The params are decoded into a fresh T before fn is called; a decode failure is reported as a CodeInvalidParams error without invoking fn.

srv.Handle("add", jsonrpc.Typed(func(ctx context.Context, p AddParams) (any, error) {
    return p.A + p.B, nil
}))

type Option

type Option func(*Server)

Option configures a Server.

func WithErrorHandler

func WithErrorHandler(fn func(method string, err error)) Option

WithErrorHandler registers a hook invoked whenever a handler returns a non-nil error or panics — including for notifications, whose errors are otherwise invisible. It is intended for logging and must not block.

func WithSerialDispatch

func WithSerialDispatch() Option

WithSerialDispatch disables concurrent dispatch. Batch elements and stream messages are then processed one at a time in arrival order. By default the server dispatches each message (and each batch element) concurrently.

type Peer

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

Peer is a bidirectional JSON-RPC 2.0 connection over a pair of byte streams. Unlike a Client (outbound only) or a Server served via Server.ServeStream (inbound only), a Peer does both at once over the same stream: it sends outbound requests and correlates their responses by id, AND it receives inbound requests, dispatches them to a method registry, and writes the responses back.

This is the shape protocols use when both ends act as both client and server on a single connection — for example a host that calls methods on a spawned plugin while also serving "host callback" methods the plugin invokes back (remote objects, progress, elicitation).

NewPeer starts a background reader immediately. The reader demultiplexes each incoming message, routing responses to pending outbound calls and requests to the Peer's Server. Inbound request handlers run in their own goroutine so a slow handler cannot stall delivery of outbound responses. Call Peer.Close to stop the reader and release the streams. Outbound calls go through the Peer's Client (see Peer.Client).

func NewPeer

func NewPeer(in io.Reader, out io.Writer, server *Server, opts ...PeerOption) *Peer

NewPeer returns a Peer that reads JSON-RPC messages from in and writes them to out, dispatching inbound requests to server (pass NewServer if you have no existing registry) and correlating outbound call responses by id.

NewPeer does not start the reader — call Peer.Serve from the goroutine that should own the input. That lets a caller finish wiring up state the handlers depend on (for example, pointing a server runtime at the peer) before any handler can run, avoiding a construction race.

func NewProcessPeer

func NewProcessPeer(name string, args []string, server *Server, opts ...ProcessOption) (*Peer, error)

NewProcessPeer spawns name with args as a JSON-RPC 2.0 peer speaking newline-delimited JSON over its stdin/stdout, and returns a bidirectional Peer connected to it. It is the subprocess counterpart to NewPeer, just as NewProcessTransport is the subprocess counterpart to NewStreamTransport: use it when the child both handles requests and makes reverse-direction requests (host callbacks, remote objects, elicitation).

It accepts the same options as NewProcessTransport (WithStderr, WithShutdownTimeout, WithEnv, WithDir, WithOnExit). Closing the Peer closes the child's stdin, waits up to the shutdown timeout, then kills it if necessary; the OnExit hook fires once after the child is reaped.

func (*Peer) Call

func (p *Peer) Call(ctx context.Context, req *Request) (*Response, error)

Call implements Transport for the Peer's internal Client. It is exported only to satisfy the interface; callers invoke outbound calls via Peer.Client.

func (*Peer) CallBatch

func (p *Peer) CallBatch(ctx context.Context, reqs []*Request) ([]*Response, error)

CallBatch implements the batch-capable transport extension used by Client.CallBatch: it writes the whole batch as a single JSON-RPC array line and collects each response by id as it arrives.

func (*Peer) Client

func (p *Peer) Client() *Client

Client returns the outbound client used to make requests and send notifications that the other end will serve:

var result T
if err := peer.Client().Call(ctx, "remote.method", params, &result); err != nil { ... }

The client shares the Peer's connection and is safe for concurrent use.

func (*Peer) Close

func (p *Peer) Close() error

Close stops the read loop, cancels contexts passed to in-flight inbound handlers, and runs the configured close function (if any) exactly once. Pending and future outbound calls fail.

func (*Peer) Done

func (p *Peer) Done() <-chan struct{}

Done returns a channel that is closed when the reader exits — on EOF from in, a fatal decode error, or Close. Err returns the reason (nil for a clean EOF).

func (*Peer) Err

func (p *Peer) Err() error

Err returns the read error that stopped the reader, or nil if it ended cleanly (EOF) or the Peer was closed without a read error.

func (*Peer) Handle

func (p *Peer) Handle(method string, h Handler) *Server

Handle registers an inbound handler on the Peer's Server; it is a convenience for p.Server().Handle. It returns the server for chaining.

func (*Peer) Notify

func (p *Peer) Notify(ctx context.Context, req *Request) error

Notify implements Transport.

func (*Peer) Serve

func (p *Peer) Serve() error

Serve runs the read loop until the input reaches EOF or a fatal decode error, returning nil for a clean EOF and the error otherwise. Serve blocks; inbound handlers run concurrently in their own goroutines. Call Peer.Wait after Serve returns to block until any still-running handlers have flushed.

func (*Peer) Server

func (p *Peer) Server() *Server

Server returns the inbound method registry. Handlers may be registered before or after NewPeer.

func (*Peer) Unregister

func (p *Peer) Unregister(method string) bool

Unregister removes an inbound handler; it is a convenience for p.Server().Unregister.

func (*Peer) Wait

func (p *Peer) Wait()

Wait blocks until every in-flight inbound handler has finished, so any response still being written when the reader exited is flushed before Wait returns. Call it after <-Done (or Close) when serving must not return before all handlers are quiesced — for example a stdio server exiting on EOF.

type PeerOption

type PeerOption func(*Peer)

PeerOption configures a Peer.

func WithPeerCloseFunc

func WithPeerCloseFunc(fn func() error) PeerOption

WithPeerCloseFunc registers a function run once when the Peer is closed, after the reader is signalled to stop. Use it to release resources backing the streams (for example, close a subprocess's stdin and reap it via NewProcessTransport's close function).

type ProcessOption

type ProcessOption func(*processConfig)

ProcessOption configures a subprocess transport created by NewProcessTransport.

func WithDir

func WithDir(dir string) ProcessOption

WithDir sets the working directory for the child process.

func WithEnv

func WithEnv(env []string) ProcessOption

WithEnv sets the environment for the child process (as in os/exec Cmd.Env). It replaces the entire environment: every variable the child sees must be listed, including PATH, HOME, etc. For the more common case of adding or overriding a few variables while keeping the parent environment, use WithExtraEnv instead.

func WithExtraEnv added in v0.2.0

func WithExtraEnv(kv ...string) ProcessOption

WithExtraEnv adds or overrides the given KEY=VALUE environment variables in the child process's environment, on top of the environment it would otherwise inherit (the parent's, or the full replacement set by WithEnv). Entries use os.Environ's "KEY=VALUE" form. Multiple WithExtraEnv options accumulate; for a repeated key the last value wins.

Unlike WithEnv (which replaces the whole environment), WithExtraEnv keeps the parent environment intact and only changes the listed variables — so PATH, HOME, etc. are still available to the child.

func WithOnExit

func WithOnExit(fn func(error)) ProcessOption

WithOnExit registers a callback invoked exactly once when the child process exits — whether it exits on its own or as a result of closing the transport. The callback receives the error returned by the child's wait (nil for a clean exit).

The callback is invoked from a dedicated goroutine after the child has been reaped, so it fires even if StreamTransport.Close is never called (e.g. the child crashes mid-session). It must not block for long and its completion is not waited on by Close; do slow cleanup on a separate goroutine.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) ProcessOption

WithShutdownTimeout sets how long Close waits for the child to exit after its stdin is closed before killing it. The default is 5 seconds.

func WithStderr

func WithStderr(w io.Writer) ProcessOption

WithStderr routes the child process's standard error to w. By default it is inherited from the parent (os.Stderr). Pass io.Discard to silence it.

type Request

type Request struct {
	JSONRPC json.RawMessage `json:"jsonrpc,omitempty"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params,omitempty"`
	ID      json.RawMessage `json:"id,omitempty"`
}

Request is a single inbound JSON-RPC request or notification.

JSONRPC, Params and ID are kept as raw JSON. ID is raw so an absent id (notification) is distinguishable from an explicit null and so the original id shape round-trips into the response unchanged. Params is raw so handlers control their own decoding. JSONRPC is raw so a wrong-typed version field yields an Invalid Request error rather than failing the whole batch decode.

func (*Request) IsNotification

func (r *Request) IsNotification() bool

IsNotification reports whether the request is a notification, i.e. it carries no id member and therefore expects no response.

type Response

type Response struct {
	Result json.RawMessage
	Error  *Error
	ID     json.RawMessage
}

Response is a single outbound JSON-RPC response.

Exactly one of Result or Error is meaningful: on success Result holds the (already marshalled) result value and Error is nil; on failure Error is set. The custom MarshalJSON guarantees the wire form matches the spec — a success response always includes a result member (null when the handler returned nothing) and never an error member, and vice versa.

func (Response) MarshalJSON

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

MarshalJSON renders the response in spec-compliant form.

func (*Response) UnmarshalJSON

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

UnmarshalJSON parses a response from the wire.

type Server

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

Server is a JSON-RPC 2.0 method registry and dispatcher. It is safe for concurrent use: handlers may be registered before serving, and dispatch is concurrent by default. The zero value is not usable; create one with NewServer.

A Server is transport-agnostic. Server.ServeHTTP exposes it as an http.Handler, Server.ServeStream runs the newline-delimited loop used for stdio, and Server.HandleMessage / Server.HandleRequest expose the dispatch core directly for embedding in other transports.

func NewServer

func NewServer(opts ...Option) *Server

NewServer creates a Server with no methods registered.

func (*Server) Handle

func (s *Server) Handle(method string, h Handler) *Server

Handle registers h for the given method name, replacing any existing handler. It returns the server to allow chaining. It panics if h is nil.

func (*Server) HandleMessage

func (s *Server) HandleMessage(ctx context.Context, raw []byte) ([]byte, bool)

HandleMessage dispatches one raw inbound JSON-RPC message — a single request, a single notification, or a batch array. It returns the marshalled response and whether a response is due. A notification (or an all-notification batch) yields (nil, false); every other message yields (bytes, true), including parse and invalid-request errors.

func (*Server) HandleRequest

func (s *Server) HandleRequest(ctx context.Context, req *Request) *Response

HandleRequest dispatches a single already-decoded request. It returns the response to send, or nil when no response is due (i.e. the request is a notification, valid or not). This is the lowest-level dispatch entry point; most callers use Server.HandleMessage.

func (*Server) Methods

func (s *Server) Methods() []string

Methods returns the sorted-order-independent set of registered method names.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP exposes the server as an http.Handler. It accepts POST requests carrying a single JSON-RPC object or a batch array. A message that produces no response (a notification, or an all-notification batch) yields 204 No Content; otherwise the JSON response is returned with a 200 status.

func (*Server) ServeStream

func (s *Server) ServeStream(ctx context.Context, in io.Reader, out io.Writer) error

ServeStream runs a newline-delimited JSON-RPC loop over the given streams, the transport used for stdio servers. It reads JSON values from in (they need not be newline-separated; any JSON whitespace works) and writes each response as a single line to out. Requests are dispatched concurrently by default and writes are serialised.

The call blocks until in reaches EOF (returning the first write error seen, if any) or a fatal decode error occurs. A decode error means the stream can no longer be framed reliably, so a parse-error response is emitted and the error is returned. Cancelling ctx cancels the context passed to in-flight handlers but does not itself unblock a pending read; close in to stop the loop.

func (*Server) Unregister

func (s *Server) Unregister(method string) bool

Unregister removes the handler for method, if any. It reports whether a handler was present.

type StreamOption

type StreamOption func(*StreamTransport)

StreamOption configures a StreamTransport.

func WithCloseFunc

func WithCloseFunc(fn func() error) StreamOption

WithCloseFunc registers a function run once when the transport is closed, after the read loop is signalled to stop. NewProcessTransport uses it to shut down the child process.

type StreamTransport

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

StreamTransport is a Transport over a pair of byte streams carrying newline-delimited JSON-RPC messages. It is the client-side counterpart to Server.ServeStream and backs both in-process pipes and subprocess communication (see NewProcessTransport).

A background goroutine reads responses from the input stream and correlates them to pending calls by request id, so many calls and notifications may be in flight concurrently. It is safe for concurrent use.

func NewProcessTransport

func NewProcessTransport(name string, args []string, opts ...ProcessOption) (*StreamTransport, error)

NewProcessTransport spawns name with args as a JSON-RPC server speaking newline-delimited JSON over its stdin/stdout, and returns a transport connected to it. Closing the transport closes the child's stdin (signalling it to exit), waits up to the shutdown timeout, then kills it if necessary. For a bidirectional connection use NewProcessPeer instead.

func NewStreamTransport

func NewStreamTransport(in io.Reader, out io.Writer, opts ...StreamOption) *StreamTransport

NewStreamTransport creates a transport that writes requests to out and reads responses from in. It starts the background read loop immediately; call StreamTransport.Close to stop it and release resources.

func (*StreamTransport) Call

func (t *StreamTransport) Call(ctx context.Context, req *Request) (*Response, error)

Call implements Transport.

func (*StreamTransport) CallBatch

func (t *StreamTransport) CallBatch(ctx context.Context, reqs []*Request) ([]*Response, error)

CallBatch implements the batch-capable transport extension used by Client.CallBatch: it writes the whole batch as a single JSON-RPC array line and collects each response as it arrives, in any order, via the same id-correlated delivery used by Call.

func (*StreamTransport) Close

func (t *StreamTransport) Close() error

Close implements Transport. It stops the read loop and runs the configured close function (if any) exactly once.

func (*StreamTransport) Notify

func (t *StreamTransport) Notify(ctx context.Context, req *Request) error

Notify implements Transport.

type Transport

type Transport interface {
	// Call sends a request and returns the correlated response. The request
	// always carries an id. Implementations must return a non-nil *Response on
	// success (which may itself carry a JSON-RPC Error), or a non-nil error for
	// transport-level failures.
	Call(ctx context.Context, req *Request) (*Response, error)
	// Notify sends a notification (a request with no id) for which no response
	// is expected. It returns only transport-level errors.
	Notify(ctx context.Context, req *Request) error
	// Close releases any resources held by the transport.
	Close() error
}

Transport is the client-side connection to a JSON-RPC endpoint. A single Transport may be shared by concurrent Client calls; implementations must be safe for concurrent use.

Directories

Path Synopsis
examples
http-client command
Command http-client calls the http-server example.
Command http-client calls the http-server example.
http-server command
Command http-server exposes a small calculator over JSON-RPC 2.0 on HTTP.
Command http-server exposes a small calculator over JSON-RPC 2.0 on HTTP.
peer-client command
Command peer-client is the host side of a bidirectional JSON-RPC 2.0 connection.
Command peer-client is the host side of a bidirectional JSON-RPC 2.0 connection.
peer-plugin command
Command peer-plugin is the server side of a bidirectional JSON-RPC 2.0 connection.
Command peer-plugin is the server side of a bidirectional JSON-RPC 2.0 connection.
stdio-client command
Command stdio-client launches a JSON-RPC stdio server as a subprocess and talks to it over the child's stdin/stdout.
Command stdio-client launches a JSON-RPC stdio server as a subprocess and talks to it over the child's stdin/stdout.
stdio-server command
Command stdio-server is a JSON-RPC 2.0 server that speaks newline-delimited JSON over stdin/stdout.
Command stdio-server is a JSON-RPC 2.0 server that speaks newline-delimited JSON over stdin/stdout.

Jump to

Keyboard shortcuts

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