The jsonrpc2 package provides a minimal but complete implementation of the JSON RPC 2.0 specification. It is designed for compatibility with other implementations at the wire level and is commonly used as the protocol foundation for Language Server Protocol (LSP) implementations.
import (
"context"
"encoding/json"
"io"
"net"
"time"
"golang.org/x/exp/jsonrpc2"
)package main
import (
"context"
"fmt"
"golang.org/x/exp/jsonrpc2"
"log"
)
// Example 1: Creating a simple client connection
func exampleClientConnection() {
ctx := context.Background()
// Dial a server
dialer := jsonrpc2.NetDialer("tcp", "localhost:8080", net.Dialer{})
conn, err := jsonrpc2.Dial(ctx, dialer, jsonrpc2.ConnectionOptions{
Handler: jsonrpc2.HandlerFunc(handleMessage),
})
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Make an RPC call
call := conn.Call(ctx, "add", []int{2, 3})
var result int
if err := call.Await(ctx, &result); err != nil {
log.Fatal(err)
}
fmt.Printf("Result: %d\n", result)
}
// Example 2: Creating a simple server
func exampleServer() {
ctx := context.Background()
// Create a listener
listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:8080",
jsonrpc2.NetListenOptions{})
if err != nil {
log.Fatal(err)
}
// Create a handler
handler := jsonrpc2.HandlerFunc(func(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
switch req.Method {
case "add":
var nums []int
if err := json.Unmarshal(req.Params, &nums); err != nil {
return nil, jsonrpc2.NewError(-32602, "Invalid params")
}
if len(nums) != 2 {
return nil, jsonrpc2.NewError(-32602, "Expected two numbers")
}
return nums[0] + nums[1], nil
default:
return nil, jsonrpc2.ErrMethodNotFound
}
})
// Start the server
server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{
Handler: handler,
})
if err != nil {
log.Fatal(err)
}
// Wait for server to finish
if err := server.Wait(); err != nil {
log.Fatal(err)
}
}
// Example 3: Handling incoming messages
func handleMessage(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
switch req.Method {
case "test":
return map[string]string{"status": "ok"}, nil
default:
return nil, jsonrpc2.ErrMethodNotFound
}
}
// Example 4: Async response handling
func exampleAsyncHandler() jsonrpc2.HandlerFunc {
return func(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
if req.Method == "slow_operation" {
// Return async response indicator
go func() {
// Simulate async work
result := map[string]string{"status": "completed"}
// Note: In real code, we'd need the connection to call Respond
_ = result
}()
return nil, jsonrpc2.ErrAsyncResponse
}
return nil, jsonrpc2.ErrNotHandled
}
}Establish outbound connections to a JSON RPC server.
func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error)Parameters:
ctx - Context for cancellation and timeoutsdialer - Dialer interface to establish the connectionbinder - Binder to configure connection optionsReturns: Configured Connection or error
Description: Uses the dialer to make a new connection, wraps the returned reader and writer, and builds a connection on top of that stream using the binder.
type Dialer interface {
Dial(ctx context.Context) (io.ReadWriteCloser, error)
}Description: Dialer is used by clients to dial a server and return a byte stream.
func NetDialer(network, address string, nd net.Dialer) DialerParameters:
network - Network type (e.g., "tcp", "unix")address - Server addressnd - net.Dialer for customizationReturns: A Dialer using the supplied standard network dialer
type Connection struct {
// Has unexported fields
}Description: Connection manages the jsonrpc2 protocol, connecting responses back to their calls. Connection is bidirectional; it does not have a designated server or client end.
func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCallParameters:
ctx - Context for the RPC callmethod - RPC method nameparams - Method parameters (marshaled to JSON)Returns: AsyncCall object to await response
Description: Invokes the target method and returns an object that can be used to await the response. The params will be marshaled to JSON before sending. You do not have to wait for the response if not needed.
func (c *Connection) Notify(ctx context.Context, method string, params interface{}) errorParameters:
ctx - Context for the notificationmethod - RPC method nameparams - Method parameters (marshaled to JSON)Returns: Error if notification fails to send
Description: Invokes the target method but does not wait for a response. The params will be marshaled to JSON before sending over the wire.
func (c *Connection) Respond(id ID, result interface{}, rerr error) errorParameters:
id - ID of the request to respond toresult - Response result (ignored if rerr is set)rerr - Error response (if any)Returns: Error if respond fails
Description: Delivers a response to an incoming Call. It is an error to not call this exactly once for any message for which a handler has previously returned ErrAsyncResponse.
func (c *Connection) Cancel(id ID)Parameters:
id - ID of the inbound message to cancelDescription: Cancel is used to cancel an inbound message by ID. Does not cancel outgoing messages. Only used inside a message handler layering cancellation protocol on top of JSON RPC 2.
func (c *Connection) Close() errorReturns: Error if close operation fails
Description: Close can be used to close the underlying stream and wait for the connection to fully shut down. Does not cancel in-flight requests but waits for them to gracefully complete.
func (c *Connection) Wait() errorReturns: Error if the connection encountered an error
Description: Wait blocks until the connection is fully closed but does not close it.
type AsyncCall struct {
// Has unexported fields
}Description: Represents an asynchronous RPC call that can be awaited for results.
func (a *AsyncCall) Await(ctx context.Context, result interface{}) errorParameters:
ctx - Context for waitingresult - Pointer to unmarshal JSON response intoReturns: Error from the RPC response or operation
Description: Await the results of a Call. The response will be unmarshaled from JSON into the result parameter.
func (a *AsyncCall) ID() IDReturns: ID used for this call
Description: ID used for this call. Can be used to cancel the call if needed.
func (a *AsyncCall) IsReady() boolReturns: true if result is ready, false otherwise
Description: IsReady can be used to check if the result is already prepared. Guaranteed to return true on a result for which Await has already returned, or a call that failed to send.
func Serve(ctx context.Context, listener Listener, binder Binder) (*Server, error)Parameters:
ctx - Context for server lifecyclelistener - Listener to accept incoming connectionsbinder - Binder to configure each connectionReturns: Running Server or error
Description: Starts a new server listening for incoming connections. Returns a fully running server; it does not block on the listener. Call Wait to block on the server or Shutdown to terminate gracefully.
type Server struct {
// Has unexported fields
}Description: Server is a running server that is accepting incoming connections.
func (s *Server) Wait() errorReturns: Error when server shuts down
Description: Wait returns only when the server has shut down.
type Listener interface {
Accept(context.Context) (io.ReadWriteCloser, error)
Close() error
Dialer() Dialer
}Description: Listener is implemented by protocols to accept new inbound connections.
func NetListener(ctx context.Context, network, address string, options NetListenOptions) (Listener, error)Parameters:
ctx - Context for listener creationnetwork - Network type (e.g., "tcp", "unix")address - Listen addressoptions - NetListenOptions for configurationReturns: Listener or error
Description: Returns a new Listener that listens on a socket using the net package.
type NetListenOptions struct {
NetListenConfig net.ListenConfig
NetDialer net.Dialer
}Description: NetListenOptions is the optional arguments to the NetListener function.
func NetPipe(ctx context.Context) (Listener, error)Parameters:
ctx - Context for pipe creationReturns: Listener using net.Pipe or error
Description: Returns a new Listener that listens using net.Pipe. Only possible to connect using the Dialer returned by the Dialer method. Each call generates a new pipe.
func NewIdleListener(timeout time.Duration, wrap Listener) ListenerParameters:
timeout - Idle timeout durationwrap - Underlying listener to wrapReturns: Wrapped Listener with idle timeout
Description: Wraps a listener with an idle timeout. When there are no active connections for at least the timeout duration, a call to Accept will fail with ErrIdleTimeout.
type Handler interface {
Handle(ctx context.Context, req *Request) (interface{}, error)
}Description: Handler handles messages on a connection. For calls, it must return a value or an error for the reply.
type HandlerFunc func(ctx context.Context, req *Request) (interface{}, error)Description: HandlerFunc is a function adapter for Handler interface.
func (f HandlerFunc) Handle(ctx context.Context, req *Request) (interface{}, error)type Preempter interface {
Preempt(ctx context.Context, req *Request) (interface{}, error)
}Description: Preempter handles messages on a connection before they are queued to the main handler. Primarily used for cancel handlers or notifications for which out-of-order processing is not an issue.
type Preempter interface {
Preempt(ctx context.Context, req *Request) (interface{}, error)
}type Request struct {
ID ID
Method string
Params json.RawMessage
}Fields:
ID - Identifier for this request (nil for notifications)Method - String containing the method name to invokeParams - Parameters as a struct or array in JSONDescription: Request is a Message sent to a peer to request behavior. If it has an ID, it is a call; otherwise, it is a notification.
func NewCall(id ID, method string, params interface{}) (*Request, error)Parameters:
id - Request identifiermethod - Method nameparams - Method parametersReturns: Constructed Call Request or error
Description: Constructs a new Call message for the supplied ID, method, and parameters.
func NewNotification(method string, params interface{}) (*Request, error)Parameters:
method - Method nameparams - Method parametersReturns: Constructed Notification Request or error
Description: Constructs a new Notification message for the supplied method and parameters.
func (msg *Request) IsCall() boolReturns: true if this request is a call (has ID), false if notification
type Response struct {
Result json.RawMessage
Error error
ID ID
}Fields:
Result - Response contentError - Error if the call failedID - ID of the request this responds toDescription: Response is a Message used as a reply to a call Request. It has the same ID as the call it responds to.
func NewResponse(id ID, result interface{}, rerr error) (*Response, error)Parameters:
id - Request ID to respond toresult - Response result (ignored if rerr is set)rerr - Error response (if any)Returns: Constructed Response or error
Description: Constructs a new Response message that is a reply to the supplied request. If rerr is set, result may be ignored.
type Message interface {
// Has unexported methods
}Description: Message is the interface to all jsonrpc2 message types. They share no common functionality but are a closed set of concrete types (*Request and *Response) allowed to implement this interface.
func DecodeMessage(data []byte) (Message, error)Parameters:
data - JSON encoded message bytesReturns: Decoded Message (Request or Response) or error
Description: Decodes a JSON byte array into a Message.
func EncodeMessage(msg Message) ([]byte, error)Parameters:
msg - Message to encodeReturns: JSON encoded message bytes or error
Description: Encodes a Message into JSON byte array.
type ID struct {
// Has unexported fields
}Description: ID is a Request identifier.
func Int64ID(i int64) IDParameters:
i - Integer identifier valueReturns: ID created from integer
Description: Creates a new integer request identifier.
func StringID(s string) IDParameters:
s - String identifier valueReturns: ID created from string
Description: Creates a new string request identifier.
func (id ID) IsValid() boolReturns: true if the ID is a valid identifier, false otherwise
Description: IsValid returns true if the ID is a valid identifier. The default value for ID returns false.
func (id ID) Raw() interface{}Returns: The underlying value of the ID
Description: Raw returns the underlying value of the ID (string, int64, or nil).
type Binder interface {
Bind(context.Context, *Connection) (ConnectionOptions, error)
}Description: Binder builds a connection configuration. Used in servers to generate a new configuration per connection. ConnectionOptions itself implements Binder, returning itself unmodified for simple cases.
type ConnectionOptions struct {
Framer Framer
Preempter Preempter
Handler Handler
}Fields:
Framer - Message framing and encoding (defaults to HeaderFramer)Preempter - Pre-queue message handler (optional)Handler - Main queued message handler (defaults to returning ErrNotHandled)Description: ConnectionOptions holds the options for new connections.
func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions, error)Description: Bind returns the options unmodified, implementing the Binder interface.
type Framer interface {
Reader(rw io.Reader) Reader
Writer(rw io.Writer) Writer
}Description: Framer wraps low-level byte readers and writers into jsonrpc2 message readers and writers. Responsible for framing and encoding of messages into wire form.
func HeaderFramer() FramerReturns: New Framer using HTTP-style headers
Description: Returns a new Framer. Messages are sent with HTTP content-length and MIME type headers. This is the format used by LSP and others.
func RawFramer() FramerReturns: New Framer using raw format
Description: Returns a new Framer. Messages are sent with no wrapping and rely on JSON decode consistency to determine message boundaries.
type Reader interface {
Read(context.Context) (Message, int64, error)
}Description: Reader abstracts transport mechanics from the JSON RPC protocol. A Connection reads messages from the reader, assuming each call to Read fully transfers a single message or returns an error. Not safe for concurrent use.
type Writer interface {
Write(context.Context, Message) (int64, error)
}Description: Writer abstracts transport mechanics from the JSON RPC protocol. A Connection writes messages using the writer, assuming each call to Write fully transfers a single message or returns an error. Not safe for concurrent use.
var (
// ErrUnknown is used for all non-coded errors
ErrUnknown = NewError(-32001, "JSON RPC unknown error")
// ErrParse is used when invalid JSON was received by the server
ErrParse = NewError(-32700, "JSON RPC parse error")
// ErrInvalidRequest is used when the JSON sent is not a valid Request object
ErrInvalidRequest = NewError(-32600, "JSON RPC invalid request")
// ErrMethodNotFound should be returned when the method does not exist
ErrMethodNotFound = NewError(-32601, "JSON RPC method not found")
// ErrInvalidParams should be returned when method parameters were invalid
ErrInvalidParams = NewError(-32602, "JSON RPC invalid params")
// ErrInternal indicates a failure to process a call correctly
ErrInternal = NewError(-32603, "JSON RPC internal error")
// ErrServerOverloaded is returned when a message was refused due to
// server being temporarily unable to accept new messages
ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded")
)var (
// ErrIdleTimeout is returned when serving timed out waiting for new connections
ErrIdleTimeout = errors.New("timed out waiting for new connections")
// ErrNotHandled is returned from a handler to indicate it did not handle the message
ErrNotHandled = errors.New("JSON RPC not handled")
// ErrAsyncResponse is returned from a handler to indicate it will generate
// a response asynchronously
ErrAsyncResponse = errors.New("JSON RPC asynchronous response")
)func NewError(code int64, message string) errorParameters:
code - JSON RPC error code (negative integer)message - Human-readable error messageReturns: Error that encodes correctly on the wire
Description: NewError returns an error that will encode on the wire correctly. Standard codes are made available from this package. This function should only be used to build errors for application-specific codes as allowed by the specification.
The package exposes event counters and distributions for monitoring:
var (
// Started counts the number of started RPCs
Started = event.NewCounter("started", ...)
// Finished counts the number of finished RPCs (includes errors)
Finished = event.NewCounter("finished", ...)
// ReceivedBytes is a distribution of received bytes
ReceivedBytes = event.NewIntDistribution("received_bytes", ...)
// SentBytes is a distribution of sent bytes
SentBytes = event.NewIntDistribution("sent_bytes", ...)
// Latency is a distribution of RPC elapsed time in milliseconds
Latency = event.NewDuration("latency", ...)
)const (
// Inbound direction constant
Inbound = "in"
// Outbound direction constant
Outbound = "out"
)The package provides helper functions for creating event labels:
func Method(v string) event.LabelDescription: Creates an event label for RPC method names.
func RPCDirection(v string) event.LabelDescription: Creates an event label for RPC direction (inbound/outbound).
func RPCID(v string) event.LabelDescription: Creates an event label for RPC identifiers.
func StatusCode(v string) event.LabelDescription: Creates an event label for status codes.
Unlike traditional RPC where one end is strictly client and the other strictly server, jsonrpc2 connections are fully bidirectional. Either end can initiate requests and receive responses.
Always return appropriate JSON RPC error codes or custom errors with NewError. Handlers should return errors rather than embedding error details in results.
Implement the Handler interface to process incoming requests. Return ErrAsyncResponse to handle requests asynchronously, then call Respond when the result is ready.
This package is designed to be compatible with Language Server Protocol (LSP). The HeaderFramer is used to provide the content-length and content-type headers that LSP expects. LSP typically uses this package with: