or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

apidiff.mdconstraints.mdebnf.mderrors.mdevent.mdgorelease.mdindex.mdio-i2c.mdio-spi.mdjsonrpc2.mdmaps.mdmmap.mdmodgraphviz.mdrand.mdshiny.mdslices.mdslog.mdstats.mdsumdb.mdtrace.mdtxtar.mdtypeparams.mdutf8string.md
tile.json

jsonrpc2.mddocs/

JSON RPC 2.0 Protocol Implementation

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.

Package Information

  • Package Name: golang.org/x/exp/jsonrpc2
  • Package Type: Go module
  • Language: Go
  • Repository: golang.org/x
  • Purpose: Minimal JSON RPC 2.0 protocol implementation for LSP and other RPC use cases

Core Imports

import (
    "context"
    "encoding/json"
    "io"
    "net"
    "time"

    "golang.org/x/exp/jsonrpc2"
)

Basic Usage

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
    }
}

Capabilities

Core Connection Management

Dialing Connections

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 timeouts
  • dialer - Dialer interface to establish the connection
  • binder - Binder to configure connection options

Returns: 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) Dialer

Parameters:

  • network - Network type (e.g., "tcp", "unix")
  • address - Server address
  • nd - net.Dialer for customization

Returns: A Dialer using the supplied standard network dialer

Connection Methods

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{}) *AsyncCall

Parameters:

  • ctx - Context for the RPC call
  • method - RPC method name
  • params - 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{}) error

Parameters:

  • ctx - Context for the notification
  • method - RPC method name
  • params - 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) error

Parameters:

  • id - ID of the request to respond to
  • result - 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 cancel

Description: 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() error

Returns: 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() error

Returns: Error if the connection encountered an error

Description: Wait blocks until the connection is fully closed but does not close it.

Async Call Handling

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{}) error

Parameters:

  • ctx - Context for waiting
  • result - Pointer to unmarshal JSON response into

Returns: 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() ID

Returns: ID used for this call

Description: ID used for this call. Can be used to cancel the call if needed.

func (a *AsyncCall) IsReady() bool

Returns: 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.

Server Management

Starting a Server

func Serve(ctx context.Context, listener Listener, binder Binder) (*Server, error)

Parameters:

  • ctx - Context for server lifecycle
  • listener - Listener to accept incoming connections
  • binder - Binder to configure each connection

Returns: 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() error

Returns: Error when server shuts down

Description: Wait returns only when the server has shut down.

Listeners

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 creation
  • network - Network type (e.g., "tcp", "unix")
  • address - Listen address
  • options - NetListenOptions for configuration

Returns: 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 creation

Returns: 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) Listener

Parameters:

  • timeout - Idle timeout duration
  • wrap - Underlying listener to wrap

Returns: 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.

Message Handling

Request Handling

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)

Preemption

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)
}

Message Types

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 invoke
  • Params - Parameters as a struct or array in JSON

Description: 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 identifier
  • method - Method name
  • params - Method parameters

Returns: 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 name
  • params - Method parameters

Returns: Constructed Notification Request or error

Description: Constructs a new Notification message for the supplied method and parameters.

func (msg *Request) IsCall() bool

Returns: true if this request is a call (has ID), false if notification

Responses

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

Fields:

  • Result - Response content
  • Error - Error if the call failed
  • ID - ID of the request this responds to

Description: 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 to
  • result - 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.

Message Encoding/Decoding

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 bytes

Returns: 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 encode

Returns: JSON encoded message bytes or error

Description: Encodes a Message into JSON byte array.

Request Identifiers

type ID struct {
    // Has unexported fields
}

Description: ID is a Request identifier.

func Int64ID(i int64) ID

Parameters:

  • i - Integer identifier value

Returns: ID created from integer

Description: Creates a new integer request identifier.

func StringID(s string) ID

Parameters:

  • s - String identifier value

Returns: ID created from string

Description: Creates a new string request identifier.

func (id ID) IsValid() bool

Returns: 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).

Connection Configuration

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.

Framers

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() Framer

Returns: 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() Framer

Returns: 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.

Stream Readers and Writers

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.

Error Handling

Standard JSON RPC Error Variables

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")
)

Handler-Specific Errors

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")
)

Creating Custom Errors

func NewError(code int64, message string) error

Parameters:

  • code - JSON RPC error code (negative integer)
  • message - Human-readable error message

Returns: 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.

Metrics and Instrumentation

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", ...)
)

Constants

const (
    // Inbound direction constant
    Inbound = "in"

    // Outbound direction constant
    Outbound = "out"
)

Event Labels

The package provides helper functions for creating event labels:

func Method(v string) event.Label

Description: Creates an event label for RPC method names.

func RPCDirection(v string) event.Label

Description: Creates an event label for RPC direction (inbound/outbound).

func RPCID(v string) event.Label

Description: Creates an event label for RPC identifiers.

func StatusCode(v string) event.Label

Description: Creates an event label for status codes.

Design Patterns

Bidirectional Communication

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.

Message Types

  • Calls: Requests with an ID expecting a response
  • Notifications: Requests without an ID, no response expected
  • Responses: Replies to calls, identified by matching ID

Error Handling

Always return appropriate JSON RPC error codes or custom errors with NewError. Handlers should return errors rather than embedding error details in results.

Connection Lifecycle

  1. Create a Dialer or Listener
  2. Create a Connection via Dial or Serve
  3. Handle incoming messages via Handler/Preempter
  4. Make outgoing calls via Call or Notify
  5. Close the connection via Close

Handler Implementation

Implement the Handler interface to process incoming requests. Return ErrAsyncResponse to handle requests asynchronously, then call Respond when the result is ready.

LSP Integration

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:

  • HeaderFramer for message framing
  • JSON encoding for parameters and results
  • String-based method names following LSP conventions
  • Context timeouts for operation deadlines