Documentation
¶
Overview ¶
Package jsonrpc2 is a minimal, allocation-conscious implementation of the JSON-RPC 2.0 wire protocol.
The package is built around a reflection-free wire core plus pluggable framing, a swappable payload codec, and a bidirectional connection state machine. The wire core encodes message envelopes by appending directly into a byte buffer (EncodeMessage, AppendMessage, AppendCall, AppendNotification, AppendResponse, AppendBatch) and decodes them with a single-pass span scanner (DecodeMessage, ParseRequests), so the hot path performs no reflection and no payload copies: a served request borrows its method and params straight from the transport frame, the request bookkeeping is pooled, and dispatch is direct-return, which is what lets a void round trip measure zero allocations.
The borrow has one rule: a Handler's request, its Method, and its Params are valid only until the handler returns. Request.Clone takes an owned copy, Async clones automatically when a handler detaches, and DetachContext keeps a context alive past the handler. The same contract applies to requests decoded by DecodeMessage and ParseRequests, whose results borrow their input; responses and error members are owned. For callback-scoped parser fast paths, ScanMessageView, ScanFrameView, and AppendRequestViews expose the same kind of borrowed views over caller-owned frame bytes.
Runtime modes are explicit: Conn/Peer is bidirectional, SingleClient serializes calls with a caller-owned read loop, PipelineClient keeps concurrent client-originated calls in flight without dispatching server-initiated requests, and BatchClient exposes raw-frame batch I/O. NewChannelStreamPair supplies an in-memory encoded-frame transport for same-process peers that still want full JSON-RPC wire encoding and scanning.
The wire-model message types are a closed set of *Call, *Notification, and *Response, all of which implement the Message interface; handlers receive the concrete Request instead.
Framing ¶
A Stream adapts a byte transport to message reads and writes. Two framings are provided: newline-delimited JSON (NewNDJSONStream, compatible with the Model Context Protocol stdio transport) and LSP "Content-Length" header framing (NewHeaderStream). NewStream selects the header framing as the gopls-compatible default.
Codec ¶
The envelope is never marshaled through a codec; only the user payload (params and result) is. The payload Codec is swappable via WithCodec and defaults to encoding/json/v2 (DefaultCodec). Faster opt-in codecs live in the codec/sonic and codec/goccy subpackages, which carry their own module dependencies and never enter this package's module graph.
Serving ¶
A Conn is a symmetric peer that can both issue (Conn.Call, Conn.Notify) and answer requests via a Handler started with Conn.Go. For network servers, Serve and ListenAndServe accept connections from a net.Listener and drive each one with a StreamServer, typically built from a Handler with HandlerServer.
Index ¶
- Constants
- Variables
- func AppendBatch(dst []byte, msgs []Message) []byte
- func AppendCall(dst []byte, id ID, method string, params RawMessage) []byte
- func AppendMessage(dst []byte, msg Message) []byte
- func AppendNotification(dst []byte, method string, params RawMessage) []byte
- func AppendResponse(dst []byte, id ID, result RawMessage, err error) []byte
- func Async(ctx context.Context)
- func DetachContext(ctx context.Context) context.Context
- func EncodeMessage(msg Message) ([]byte, error)
- func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, ...) error
- func MethodNotFoundHandler(ctx context.Context, req *Request) (any, error)
- func Serve(ctx context.Context, ln net.Listener, server StreamServer, ...) error
- type BatchClient
- type BatchServer
- type Call
- type Code
- type Codec
- type Conn
- type Error
- type ErrorView
- type FrameView
- type Framer
- type Handler
- type ID
- type IDView
- func (id IDView) ID() (ID, bool)
- func (id IDView) IsNumber() bool
- func (id IDView) IsString() bool
- func (id IDView) IsValid() bool
- func (id IDView) Number() (int64, bool)
- func (id IDView) Raw() []byte
- func (id IDView) StringBytes() ([]byte, bool)
- func (id IDView) StringEscaped() bool
- func (id IDView) StringValue() (string, bool)
- type JSONCodec
- type Message
- type MessageView
- type MessageViewKind
- type Notification
- type Option
- type ParsedMessage
- type ParsedMessageView
- type Peer
- type PipelineClient
- func (c *PipelineClient) Call(ctx context.Context, method string, params, result any) (ID, error)
- func (c *PipelineClient) Close() error
- func (c *PipelineClient) Done() <-chan struct{}
- func (c *PipelineClient) Err() error
- func (c *PipelineClient) Go(ctx context.Context, _ Handler)
- func (c *PipelineClient) Notify(ctx context.Context, method string, params any) error
- type Preempter
- type RawMessage
- type Request
- type RequestMessage
- type Response
- type Server
- type ServerFunc
- type SingleClient
- type Stream
- type StreamServer
- type SyncClient
Constants ¶
const ( // ErrIdleTimeout is returned when serving timed out waiting for new // connections. ErrIdleTimeout = constError("timed out waiting for new connections") // ErrClientClosing is returned for an outgoing [Conn.Call] or [Conn.Notify] // once the connection is shutting down: the local end has called Close, or // the read or write side of the stream has failed. ErrClientClosing = constError("jsonrpc2: client is closing") // ErrServerClosing is the error written in response to an incoming call that // arrives, or is still queued, once the connection is shutting down. ErrServerClosing = constError("jsonrpc2: server is closing") )
const ErrInvalidHeader = constError("jsonrpc2: invalid message header")
ErrInvalidHeader is returned by the LSP header framer when a frame's header block is malformed: it lacks the terminating blank line, contains a field without a colon, or declares a missing, non-numeric, zero, negative, or oversized Content-Length (above [maxContentLength]).
const ErrNotHandled = constError("jsonrpc2: request not handled")
ErrNotHandled is returned by a Preempter (or a Handler) to indicate that the request was not handled and should fall through to the next stage.
const MessageViewResponse = MessageViewResponseResult
MessageViewResponse is a short alias for a successful response view.
const Version = "2.0"
Version is the JSON-RPC protocol version that every envelope advertises in its "jsonrpc" member.
Variables ¶
var ( // ErrUnknown should be used for all non-coded errors. ErrUnknown = NewError(UnknownError, "JSON-RPC unknown error") // ErrParse is used when invalid JSON was received by the server. ErrParse = NewError(ParseError, "JSON-RPC parse error") // ErrInvalidRequest is used when the JSON sent is not a valid Request object. ErrInvalidRequest = NewError(InvalidRequest, "JSON-RPC invalid request") // ErrMethodNotFound should be returned by the handler when the method does // not exist or is not available. ErrMethodNotFound = NewError(MethodNotFound, "JSON-RPC method not found") // ErrInvalidParams should be returned by the handler when the method // parameter(s) were invalid. ErrInvalidParams = NewError(InvalidParams, "JSON-RPC invalid params") // ErrInternal indicates a failure to process a call correctly. ErrInternal = NewError(InternalError, "JSON-RPC internal error") )
Standard JSON-RPC 2.0 errors, exposed as sentinels for use with errors.Is.
Functions ¶
func AppendBatch ¶ added in v1.0.0
AppendBatch appends a JSON-RPC batch array containing msgs to dst.
The function appends exactly the messages it is given. A valid JSON-RPC batch request contains at least one request/notification member, and a valid batch response contains at least one response member; callers that need to enforce those protocol roles should do so before calling AppendBatch.
func AppendCall ¶ added in v1.0.0
func AppendCall(dst []byte, id ID, method string, params RawMessage) []byte
AppendCall appends a JSON-RPC call envelope to dst.
func AppendMessage ¶ added in v1.0.0
AppendMessage appends msg's JSON-RPC envelope to dst and returns the extended slice.
Unlike EncodeMessage, AppendMessage does not allocate an owned right-sized result. The returned bytes alias dst's backing array, making this the preferred API for callers that own an output buffer or write batch envelopes directly.
func AppendNotification ¶ added in v1.0.0
func AppendNotification(dst []byte, method string, params RawMessage) []byte
AppendNotification appends a JSON-RPC notification envelope to dst.
func AppendResponse ¶ added in v1.0.0
func AppendResponse(dst []byte, id ID, result RawMessage, err error) []byte
AppendResponse appends a JSON-RPC response envelope to dst.
func Async ¶ added in v1.0.0
Async signals that the current request may be handled concurrently with requests that arrive after it. When ctx is a request context served by a connection, the read loop is released to process the next message immediately; the remainder of the handler then runs concurrently. The request's borrowed method and params are cloned in place by the release, so they remain valid until the handler returns.
Async must be called at most once per request context. Calling it on a context that does not carry a release token (for example a non-request context) is a no-op.
func DetachContext ¶ added in v1.0.0
DetachContext returns a context that is safe to retain after the handler returns. The per-request context handed to a handler is pooled and recycled with the request, so retaining it past the handler's return is illegal; DetachContext steps up to the connection-lifetime parent, keeping its values and deadline but dropping the request-scoped cancellation.
func EncodeMessage ¶ added in v1.0.0
EncodeMessage encodes a Message into a freshly allocated JSON-RPC envelope.
The envelope is built by appending directly into a pooled buffer with no reflection: method names and string identifiers are written through a fast-escape routine, integer identifiers through strconv, and params/result raw values verbatim. The returned slice is a right-sized copy that the caller owns; the pooled buffer is recycled before returning.
func ListenAndServe ¶ added in v0.7.0
func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error
ListenAndServe starts a jsonrpc2 server on the given network address.
It listens on network and addr, then serves accepted connections with Serve. The listener is closed when ListenAndServe returns; for a "unix" network the socket file is also removed. If idleTimeout is non-zero, ListenAndServe returns ErrIdleTimeout after there have been no connections for that duration; otherwise it returns only on error or when ctx is canceled.
func MethodNotFoundHandler ¶ added in v0.7.0
MethodNotFoundHandler is a Handler that answers every call with the standard "method not found" error and drops notifications. It is intended to be the final handler in a chain.
func Serve ¶ added in v0.7.0
func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error
Serve accepts incoming connections from ln and serves each with server on its own goroutine.
Serve returns when:
- the listener fails to accept (the accept error is returned);
- idleTimeout is non-zero and no connection has been active for that duration (ErrIdleTimeout is returned); or
- ctx is canceled (ctx.Err() is returned).
On any of these, Serve stops accepting, closes ln to unblock the accept goroutine, closes the streams of any still-active connections so their StreamServer goroutines unwind, waits for all of them to return, and only then returns. Serve therefore leaks no goroutine: every goroutine it starts has exited by the time it returns. Because Serve closes ln on return, the caller need not (a second Close is harmless).
Types ¶
type BatchClient ¶ added in v1.0.0
type BatchClient struct {
// contains filtered or unexported fields
}
BatchClient is the raw-frame batch client mode.
It exposes only frame primitives so batch-mode callers can write a complete JSON-RPC batch array and read the response frame without passing through the single-message Conn.Call API. The supplied Stream must be frame-capable; the built-in framers satisfy this requirement.
func NewBatchClient ¶ added in v1.0.0
func NewBatchClient(stream Stream) (*BatchClient, error)
NewBatchClient creates a raw-frame batch client over stream.
func (*BatchClient) Close ¶ added in v1.0.0
func (c *BatchClient) Close() error
Close closes the underlying stream.
func (*BatchClient) WriteFrame ¶ added in v1.0.0
WriteFrame writes one already-encoded JSON frame.
type BatchServer ¶ added in v1.0.0
type BatchServer struct {
Conn
}
BatchServer is the batch-capable server endpoint mode.
It names the existing Conn batch dispatch path explicitly. New batch-only server APIs can grow behind this type without changing single-client or peer constructors.
func NewBatchServer ¶ added in v1.0.0
func NewBatchServer(stream Stream, opts ...Option) *BatchServer
NewBatchServer creates a batch-capable server endpoint over stream.
type Call ¶ added in v0.7.0
type Call struct {
// contains filtered or unexported fields
}
Call is a request that expects a Response. The response carries a matching ID.
func NewCall ¶ added in v0.7.0
func NewCall(id ID, method string, params RawMessage) *Call
NewCall constructs a *Call for the supplied id, method, and pre-encoded parameters. The params bytes are retained verbatim; pass nil for no parameters.
func (*Call) Method ¶ added in v0.7.0
Method implements RequestMessage.
func (*Call) Params ¶ added in v0.7.0
func (c *Call) Params() RawMessage
Params implements RequestMessage.
type Code ¶
type Code int32
Code is an error code as defined by the JSON-RPC 2.0 specification.
See https://www.jsonrpc.org/specification#error_object for details.
const ( // ParseError indicates that invalid JSON was received by the server, or that // an error occurred on the server while parsing the JSON text. ParseError Code = -32700 // InvalidRequest indicates that the JSON sent is not a valid Request object. InvalidRequest Code = -32600 // MethodNotFound indicates that the method does not exist or is not // available. MethodNotFound Code = -32601 // InvalidParams indicates invalid method parameter(s). InvalidParams Code = -32602 // InternalError is the internal JSON-RPC error. InternalError Code = -32603 // JSONRPCReservedErrorRangeStart is the start of the JSON-RPC reserved error // code range. // // It does not denote a real error code. No LSP error codes should be defined // between the start and end of the range. For backwards compatibility the // ServerNotInitialized and UnknownError codes are left in the range. // // @since 3.16.0. JSONRPCReservedErrorRangeStart Code = -32099 // CodeServerErrorStart is reserved for implementation-defined server errors. // // Deprecated: Use JSONRPCReservedErrorRangeStart instead. CodeServerErrorStart = JSONRPCReservedErrorRangeStart // ServerNotInitialized indicates that the server has not been initialized. ServerNotInitialized Code = -32002 // UnknownError should be used for all non-coded errors. UnknownError Code = -32001 // JSONRPCReservedErrorRangeEnd is the end of the JSON-RPC reserved error code // range. // // It does not denote a real error code. // // @since 3.16.0. JSONRPCReservedErrorRangeEnd Code = -32000 // CodeServerErrorEnd is reserved for implementation-defined server errors. // // Deprecated: Use JSONRPCReservedErrorRangeEnd instead. CodeServerErrorEnd = JSONRPCReservedErrorRangeEnd )
Standard JSON-RPC 2.0 error codes and the LSP-reserved range.
type Codec ¶ added in v1.0.0
type Codec interface {
// Marshal encodes v into its JSON representation. A nil v encodes to the JSON
// null literal. A [RawMessage] value is returned verbatim.
Marshal(v any) ([]byte, error)
// Unmarshal decodes the JSON in data into the value pointed to by v. When v is
// a *RawMessage the bytes are copied verbatim into a slice the caller owns.
Unmarshal(data []byte, v any) error
}
Codec marshals and unmarshals the user payloads of a JSON-RPC message: the "params" of a request and the "result" of a response. The envelope itself is never routed through a Codec; only the user-controlled payload is.
Implementations must be safe for concurrent use, must not escape HTML-sensitive characters (to match a json.Encoder with SetEscapeHTML(false)), and must treat a RawMessage (or encoding/json.RawMessage) as an opaque, already-encoded value that is passed through verbatim without re-encoding.
DefaultCodec is the Codec used for payloads when a connection is not given an explicit codec. It is backed by the encoding/json/v2 experiment (github.com/go-json-experiment/json) and is overridable by assigning a different Codec; the opt-in codec/sonic and codec/goccy packages provide drop-in alternatives.
type Conn ¶
type Conn interface {
// Call invokes method on the peer and waits for the response.
//
// params is marshaled with the connection's [Codec] before being sent; a nil
// params sends no parameters. The response result is unmarshaled into result,
// which may be nil to discard it. The returned [ID] is unique to this
// connection and is the id under which the call was sent.
//
// Call returns the error response sent by the peer, the local marshaling or
// write error, or ctx.Err() if ctx is canceled before the response arrives.
Call(ctx context.Context, method string, params, result any) (ID, error)
// Notify invokes method on the peer without waiting for a response.
//
// params is marshaled with the connection's [Codec]; a nil params sends no
// parameters.
Notify(ctx context.Context, method string, params any) error
// Go starts the connection's read goroutine, dispatching incoming requests
// to handler. It must be called exactly once per Conn and returns
// immediately; block on [Conn.Done] to wait for the connection to
// terminate.
//
// Dispatch is direct-return: handler's return values become the response,
// no per-request reply machinery is allocated, and the request is a pooled
// concrete value whose method and params are borrowed from the transport
// frame, valid until the handler returns (see [Handler] for the lifetime
// contract and [Request.Clone] for retention).
//
// By default a request handler runs inline on the read goroutine, so
// handlers observe requests in wire order and the next message is not read
// until the current handler returns. A handler that wants to overlap later
// requests (for example a long-running call, or a server that issues calls
// back to the peer) must release itself with [Async] or be wrapped with
// [AsyncHandler]; otherwise a handler that issues a server-initiated
// [Conn.Call] back into this same connection deadlocks the read goroutine,
// because the response to that call cannot be read while the read goroutine
// is blocked running the handler. A server-initiated [Conn.Notify] needs no
// release, since it never waits for a response. See [Handler] for the full
// reentrancy contract.
//
// [Conn.Close] is the authoritative teardown. Canceling ctx is observed only
// between frames: the read loop checks ctx before it starts the next frame, so
// a cancellation requests a graceful stop at the next frame boundary, but a
// reader already blocked mid-frame (waiting on the peer) is not interrupted by
// ctx-cancel. Close, by contrast, closes the underlying stream and so unblocks
// a reader parked in the middle of a frame. To guarantee prompt termination,
// call Close rather than relying on ctx cancellation. A termination caused by
// canceling ctx is treated as a clean shutdown and is not reported by
// [Conn.Err].
Go(ctx context.Context, handler Handler)
// Close stops accepting new work, waits for in-flight calls and handlers to
// drain, closes the underlying stream, and blocks until the connection has
// fully terminated (the read goroutine has exited). It reports the stream's
// close error. After Close returns, [Conn.Done] is already closed.
Close() error
// Done returns a channel that is closed when the connection has fully
// terminated: the read goroutine has exited and all in-flight work has
// drained.
Done() <-chan struct{}
// Err reports the error that terminated the connection, or nil if it was
// closed cleanly. A clean end of stream ([io.EOF]) is reported as nil.
Err() error
}
Conn is the common interface to JSON-RPC clients and servers.
Conn is bidirectional: it has no designated server or client end. It manages the JSON-RPC 2.0 protocol on top of a Stream, correlating responses back to the calls that produced them and dispatching incoming requests to a Handler.
A Conn is created with NewConn and driven by a single read goroutine that is started by Conn.Go. The same Conn may be used concurrently for outgoing Conn.Call and Conn.Notify from many goroutines.
type Error ¶
type Error struct {
// Message is a short description of the error.
Message string `json:"message"`
// Data is an optional primitive or structured value that carries additional
// information about the error. It is omitted from the wire form when the
// zero value.
Data RawMessage `json:"data,omitzero"`
// Code is a number indicating the error type that occurred.
Code Code `json:"code"`
}
Error is the wire representation of a JSON-RPC error object.
It is the structured value carried in the "error" member of a Response. It implements the error interface so that a handler can return it directly, and it preserves its Code across the error<->wire mapping performed by [toWireError].
func Errorf ¶
Errorf builds an *Error for the supplied code, using the format specifier and arguments to construct the message.
type ErrorView ¶ added in v1.0.0
type ErrorView struct {
// Raw is the borrowed raw error-object span.
Raw []byte
CodeRaw []byte
// MessageRaw is the borrowed raw string value span, including quotes.
// MessageBytes is the borrowed unquoted string body. When MessageEscaped is
// true, MessageBytes contains raw escaped body bytes.
MessageRaw []byte
MessageBytes []byte
// Data is the borrowed raw "data" value span, or nil when absent or null.
Data []byte
Code Code
MessageEscaped bool
}
ErrorView is a zero-copy view of a JSON-RPC error object.
func (*ErrorView) MessageString ¶ added in v1.0.0
MessageString decodes the error message as a Go string.
It may allocate for escaped messages; hot paths should prefer MessageBytes when MessageEscaped is false.
type FrameView ¶ added in v1.0.0
type FrameView struct {
// contains filtered or unexported fields
}
FrameView is a view of one framed JSON-RPC body.
A FrameView returned by ScanFrameView borrows the frame slice passed to it: Bytes and every byte slice reachable from MessageView alias that slice. The borrowed view is valid only until the caller mutates/reuses the frame, the stream performs the next read into the same storage, or a callback that received the view returns. Call Clone before retaining a FrameView beyond that lifetime.
A FrameView returned by NewFrameView or FrameView.Clone owns a private copy of the frame, so its borrowed spans remain valid for the FrameView's lifetime.
func NewFrameView ¶ added in v1.0.0
NewFrameView copies frame, scans the copy, and returns an owning FrameView.
Use NewFrameView at explicit lifetime boundaries where a view must outlive the read buffer or callback that produced the original frame.
func ScanFrameView ¶ added in v1.0.0
ScanFrameView scans frame as one borrowed JSON-RPC message frame.
The returned view aliases frame. Mutating or reusing frame mutates or invalidates all byte slices reachable from the returned FrameView.
func (*FrameView) Bytes ¶ added in v1.0.0
Bytes returns the raw JSON frame bytes backing this view.
For a borrowed FrameView the returned slice aliases the caller-provided frame. For an owning FrameView it aliases the FrameView's private copy.
func (*FrameView) Clone ¶ added in v1.0.0
Clone returns an owning FrameView with all spans retargeted into a private copy of the frame.
func (*FrameView) MessageView ¶ added in v1.0.0
func (v *FrameView) MessageView() MessageView
MessageView returns the parsed borrowed message view for this frame.
type Framer ¶ added in v0.7.0
type Framer func(conn io.ReadWriteCloser) Stream
Framer adapts a byte-oriented connection into a message-oriented Stream by supplying the wire framing. The same Framer value can wrap many connections.
type Handler ¶
Handler is invoked to handle an incoming Request. It is the direct-return handler shape: the returned result (or error) becomes the call's response, so dispatch needs no per-request reply machinery. To answer a call with a JSON-RPC error, return that error (an *Error is sent verbatim; any other error is wrapped). For a notification the result is discarded and a non-nil error fails the connection.
Lifetime contract: req, its Method, and its Params are valid only until the handler returns; the request struct is pooled and recycled afterward. The three legal retention patterns are:
- Copy what you need before returning (unmarshal params, copy the method).
- Take Request.Clone and retain the owned clone.
- Release with Async, which clones the request in place automatically; the request then remains valid until the (now concurrent) handler returns.
The per-request ctx likewise dies with the handler; use DetachContext for work that outlives it. Builds tagged jsonrpc2poison scribble recycled requests so violations fail loudly in tests.
Reentrancy and the deadlock-avoidance contract: in the synchronous case a Handler runs inline on the connection's read loop, which is blocked until the handler returns or releases itself. Because a Conn is symmetric, a handler may call back into the same connection while serving a request (the pattern LSP relies on), but it must observe one rule. To make a server-initiated *call* back to the peer with Conn.Call and await its response from within a handler, the handler MUST first release the read loop with Async (or be wrapped with AsyncHandler); otherwise it deadlocks, because the response to that callback must be read by the very read loop that is blocked running the handler. A server-initiated *notification* with Conn.Notify does not require Async: Notify only writes and never waits for a response.
The connection enforces a deterministic outcome for a misbehaving handler: if a handler panics, the connection answers an unanswered call with an InternalError response, then fails the connection so the panic is surfaced through Conn.Err rather than silently swallowed.
func AsyncHandler ¶ added in v0.7.0
AsyncHandler wraps handler so that every request is released for concurrent handling as soon as it is received: the read loop hands its role to a successor immediately and the wrapped handler continues concurrently.
Requests are still started in wire order, but they run concurrently and carry no mutual ordering guarantee once released. The release clones the request's borrowed spans, so the wrapped handler keeps a valid request for as long as it runs.
func CancelHandler ¶ added in v0.7.0
CancelHandler wraps handler to support cancellation by request id. It returns the wrapped handler and a canceller that, when called with the id of an in-flight call, cancels the context passed to that call's handler.
The canceller is safe for concurrent use and is a no-op for an id that is not currently being handled.
type ID ¶
type ID struct {
// contains filtered or unexported fields
}
ID is a JSON-RPC request identifier.
Per the specification an identifier is a string, an integer, or null. ID stores the value without boxing it into an interface, so constructing and encoding an integer identifier performs no heap allocation.
The zero value is a valid, unset identifier (kind none); it encodes as the JSON null literal and is used for notifications and for error responses that have no associated request id.
func NewNumberID ¶ added in v0.7.0
NewNumberID returns an ID holding the integer value v.
func NewStringID ¶ added in v0.6.2
NewStringID returns an ID holding the string value v.
func (ID) Format ¶ added in v0.6.2
Format implements fmt.Formatter.
For the %q verb the representation is unambiguous: string forms are quoted and number forms are preceded by '#'. For every other verb a string is written as its text and a number as its decimal digits. An unset identifier is written as the literal "null".
func (ID) IsNumber ¶ added in v1.0.0
IsNumber reports whether the identifier holds an integer value.
func (ID) IsValid ¶ added in v1.0.0
IsValid reports whether the identifier is set (a number or a string). The zero value reports false.
func (ID) StringValue ¶ added in v1.0.0
String returns the string value of the identifier and whether it is a string.
type IDView ¶ added in v1.0.0
type IDView struct {
// contains filtered or unexported fields
}
IDView is a zero-copy view of a JSON-RPC identifier.
StringBytes exposes the borrowed string body with surrounding quotes removed. When StringEscaped is true, those bytes are raw escaped body bytes, not decoded text; decode on demand with StringValue or ID.
func (IDView) ID ¶ added in v1.0.0
ID converts id into the package's ordinary owned ID representation.
func (IDView) IsNumber ¶ added in v1.0.0
IsNumber reports whether the identifier holds an integer value.
func (IDView) IsString ¶ added in v1.0.0
IsString reports whether the identifier holds a string value.
func (IDView) IsValid ¶ added in v1.0.0
IsValid reports whether the identifier is set (number or string).
func (IDView) Number ¶ added in v1.0.0
Number returns the integer value of the identifier and whether it is a number.
func (IDView) StringBytes ¶ added in v1.0.0
StringBytes returns the borrowed string body and whether the ID is a string.
When StringEscaped is true, the returned bytes are raw escaped body bytes; use StringValue or ID for the decoded slow path.
func (IDView) StringEscaped ¶ added in v1.0.0
StringEscaped reports whether StringBytes carries raw escaped bytes rather than decoded string bytes.
func (IDView) StringValue ¶ added in v1.0.0
StringValue decodes the identifier string. It may allocate for escaped IDs; hot paths should prefer StringBytes when possible.
type JSONCodec ¶ added in v1.0.0
type JSONCodec struct{}
JSONCodec is the default Codec implementation, backed by encoding/json/v2 (github.com/go-json-experiment/json). It produces compact output with no trailing newline and no HTML escaping, consistent with json.Marshal, and passes RawMessage values through verbatim.
func (JSONCodec) Marshal ¶ added in v1.0.0
Marshal implements Codec using encoding/json/v2. A RawMessage or encoding/json.RawMessage value is returned verbatim (with nil mapped to the null literal); every other value is encoded through json/v2.
type Message ¶ added in v0.7.0
type Message interface {
// contains filtered or unexported methods
}
Message is the interface implemented by all JSON-RPC message types.
The set of implementations is closed: only *Call, *Notification, and *Response satisfy it. The unexported method makes the set impossible to extend from outside the package.
func DecodeMessage ¶ added in v0.7.0
DecodeMessage decodes a single JSON-RPC message from data into a Message.
The decoder uses a hand-written span scanner: it locates the recognized top-level members without building a map or decoding into a reflection struct and builds the message from the recorded spans.
Lifetime contract (v2): a decoded request's method string and params RawMessage BORROW data — they are valid only until data is reused or mutated. Inside a connection the borrow is bounded by "until the handler returns"; a caller that needs the request afterward must copy what it retains (see Request.Clone for the concrete request shape). Response results and error members are still copied into owned allocations.
A batch (a value whose first non-whitespace byte is '[') is rejected; use ParseRequests to handle single-or-batch request input.
String contents (method names, string identifiers, and error messages) are decoded with JSON escape handling but are otherwise preserved verbatim and are not UTF-8-validated; invalid UTF-8 bytes are passed through unchanged rather than replaced.
type MessageView ¶ added in v1.0.0
type MessageView struct {
Error ErrorView
// JSONRPC is the borrowed raw value span of the "jsonrpc" member.
JSONRPC []byte
// IDRaw is the borrowed raw value span of the "id" member. ID is the parsed
// view of the same span. For notifications and null IDs, ID is invalid.
IDRaw []byte
// MethodRaw is the borrowed raw string value span, including quotes.
// MethodBytes is the borrowed unquoted string body. When MethodEscaped is
// true, MethodBytes contains raw escaped body bytes; use MethodString or
// Owned at explicit slow/owning boundaries.
MethodRaw []byte
MethodBytes []byte
// Params and Result are borrowed raw JSON value spans. Params is nil when the
// request omits params or explicitly sets params to null. Result preserves a
// null result as the borrowed bytes "null".
Params []byte
Result []byte
// ErrorRaw is the borrowed raw value span of the "error" member. Error is a
// parsed borrowed view of the same error object when Kind is
// MessageViewResponseError.
ErrorRaw []byte
ID IDView
Kind MessageViewKind
MethodEscaped bool
}
MessageView is a zero-copy view of a single JSON-RPC message.
Every byte slice in MessageView aliases the frame passed to ScanMessageView or ScanFrameView. The view is valid only while that frame remains valid and unmodified. Callback-scoped APIs may make this lifetime even shorter: when a view is delivered to a callback, callers must treat it as invalid after the callback returns unless they first call Clone or Owned.
The view deliberately exposes borrowed []byte spans rather than RawMessage to avoid making borrowed bytes look like the package's ordinary owned decoded message payloads.
func ScanMessageView ¶ added in v1.0.0
func ScanMessageView(frame []byte) (MessageView, error)
ScanMessageView scans a single JSON-RPC message and returns a borrowed view of its recognized fields. It performs no copies on the common no-escape path.
The returned view aliases frame. Use MessageView.Clone, MessageView.Owned, or FrameView.Clone before retaining it beyond the current read/callback lifetime.
func (*MessageView) Clone ¶ added in v1.0.0
func (v *MessageView) Clone() MessageView
Clone copies every borrowed byte span in v and returns a retained MessageView.
The returned view no longer aliases the scanned frame, but spans that referred to adjacent parts of the original frame are copied independently. Use FrameView.Clone when retaining a whole frame plus retargeted spans is more convenient.
func (*MessageView) MethodString ¶ added in v1.0.0
func (v *MessageView) MethodString() (string, bool)
MethodString decodes the method name as a Go string.
It may allocate for escaped strings and for converting unescaped bytes to a string, so hot dispatch should compare MethodBytes directly where possible.
type MessageViewKind ¶ added in v1.0.0
type MessageViewKind uint8
MessageViewKind classifies a borrowed MessageView.
const ( // MessageViewInvalid is the zero value and is not returned for a successful // scan. MessageViewInvalid MessageViewKind = iota // MessageViewCall is a request that carries a non-null identifier and expects // a response. MessageViewCall // MessageViewNotification is a request with no identifier or a null // identifier. MessageViewNotification // MessageViewResponseResult is a response carrying a result member. A JSON // null result is represented by the borrowed bytes "null". MessageViewResponseResult // MessageViewResponseError is a response carrying an error object. MessageViewResponseError )
func (MessageViewKind) String ¶ added in v1.0.0
func (k MessageViewKind) String() string
String returns a stable diagnostic name for k.
type Notification ¶ added in v0.7.0
type Notification struct {
// contains filtered or unexported fields
}
Notification is a request that does not expect a response and therefore carries no ID.
func NewNotification ¶ added in v0.7.0
func NewNotification(method string, params RawMessage) *Notification
NewNotification constructs a *Notification for the supplied method and pre-encoded parameters. The params bytes are retained verbatim; pass nil for no parameters.
func (*Notification) Method ¶ added in v0.7.0
func (n *Notification) Method() string
Method implements RequestMessage.
func (*Notification) Params ¶ added in v0.7.0
func (n *Notification) Params() RawMessage
Params implements RequestMessage.
type Option ¶ added in v1.0.0
type Option func(*conn)
Option configures a Conn created by NewConn.
func WithCodec ¶ added in v1.0.0
WithCodec sets the Codec used to marshal call params and unmarshal call results on the connection. When unset the connection uses DefaultCodec.
func WithPreempter ¶ added in v1.0.0
WithPreempter sets the Preempter consulted for every incoming request before it is dispatched to the handler. The preempter runs inline on the read goroutine; a request it handles (by returning a result and an error other than ErrNotHandled) is answered immediately and never reaches the handler.
type ParsedMessage ¶ added in v1.0.0
type ParsedMessage struct {
// Msg holds the decoded [*Call] or [*Notification] when the message is
// well-formed, and is nil when Err is set.
Msg RequestMessage
// Err describes why a malformed message could not be decoded, and is nil for
// a well-formed message.
Err *Error
// Batch reports whether the message came from a top-level JSON array.
Batch bool
}
ParsedMessage is the parsed form of one request in a single-or-batch input.
When the message is well-formed Err is nil and Msg holds the decoded *Call or *Notification. When the message is malformed Err describes why and Msg is nil; the surrounding batch is still reported so that a caller can answer the valid entries and produce error responses for the invalid ones. Batch reports whether the message came from a JSON array.
func ParseRequests ¶ added in v1.0.0
func ParseRequests(data []byte) ([]*ParsedMessage, error)
ParseRequests parses a single request or a batch of requests from data.
It returns an error only when data is not structurally valid JSON at the top level (neither a single object nor an array of objects). Per-message problems are reported in the Err field of the corresponding ParsedMessage rather than failing the whole parse, mirroring the lenient behavior expected of a server front-end.
Lifetime: each parsed message's method and params BORROW data (escape-bearing strings are decoded into owned copies). They are valid only until data is reused or mutated; copy what you retain. Ids and error members are owned.
type ParsedMessageView ¶ added in v1.0.0
type ParsedMessageView struct {
Err *Error
View MessageView
Batch bool
}
ParsedMessageView is the borrowed-view form of one request parsed from a single-or-batch input.
When Err is nil, View holds a borrowed call or notification view. When Err is non-nil, View is invalid and Err describes the per-member parse/request problem. Batch reports whether the member came from a JSON array. All byte slices reachable from View alias the input passed to ScanRequestViews or AppendRequestViews.
func AppendRequestViews ¶ added in v1.0.0
func AppendRequestViews(dst []ParsedMessageView, data []byte) ([]ParsedMessageView, error)
AppendRequestViews appends borrowed request views parsed from data to dst.
The top-level error behavior matches ParseRequests: malformed arrays return an error for the whole input, while malformed single requests or malformed batch members are represented by per-entry Err values.
func ScanRequestViews ¶ added in v1.0.0
func ScanRequestViews(data []byte) ([]ParsedMessageView, error)
ScanRequestViews parses a single request or a batch of requests into borrowed views.
It mirrors ParseRequests but yields flat span views instead of boxed Call or Notification messages (whose method and params likewise borrow data). The returned views alias data and must not outlive the current frame/callback lifetime unless cloned or converted to owned messages.
type Peer ¶ added in v1.0.0
type Peer = Conn
Peer is the bidirectional JSON-RPC endpoint mode.
It names the existing Conn capability set explicitly: both sides may send calls, notifications, and responses, with Async required for handlers that wait on server-initiated calls.
type PipelineClient ¶ added in v1.0.0
type PipelineClient struct {
// contains filtered or unexported fields
}
PipelineClient is the pipelined client mode.
It supports concurrent client-driven calls and notifications, but it does not dispatch server-initiated requests. The response reader is client-only: for frame-capable streams it scans borrowed response views and unmarshals the result directly into the waiting call's destination before the next frame can invalidate the borrowed bytes.
func NewPipelineClient ¶ added in v1.0.0
func NewPipelineClient(stream Stream, opts ...Option) *PipelineClient
NewPipelineClient creates a pipelined client over stream.
func (*PipelineClient) Close ¶ added in v1.0.0
func (c *PipelineClient) Close() error
Close implements Conn.
func (*PipelineClient) Done ¶ added in v1.0.0
func (c *PipelineClient) Done() <-chan struct{}
Done implements Conn.
func (*PipelineClient) Err ¶ added in v1.0.0
func (c *PipelineClient) Err() error
Err implements Conn.
type Preempter ¶ added in v1.0.0
type Preempter interface {
// Preempt is called for each incoming request before it is dispatched.
// Returning [ErrNotHandled] (or a nil handled value) defers the request to
// the [Handler]; returning any other result handles the request inline and
// it is not passed to the Handler.
Preempt(ctx context.Context, req *Request) (handled any, err error)
}
Preempter is an optional hook consulted for every incoming Request before it is dispatched to the Handler. It runs inline on the read loop, so it must not block; it is intended for fast, ordered handling such as "$/cancelRequest" notifications that must be observed ahead of the request they cancel. The request follows the same borrowed lifetime as a Handler's: valid only until Preempt returns.
type RawMessage ¶ added in v0.7.0
type RawMessage []byte
RawMessage is a raw, already-encoded JSON value.
It mirrors the semantics of encoding/json's RawMessage: the bytes are stored verbatim on encode and returned verbatim on decode. A RawMessage exposed by a decoded request BORROWS the decoder's input (see DecodeMessage); other decoded values own their backing arrays.
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request is the concrete request shape used by direct-return dispatch. It is a value, not an interface: the scanner fills it in place and dispatch embeds it in the per-request bookkeeping, so a request needs no message box. Method and params spans are borrowed from the transport frame and are valid until the handler returns.
func (*Request) Clone ¶ added in v1.0.0
Clone returns a copy of the request whose method and params own their bytes, safe to retain after the handler returns. It is the escape hatch for the borrowed-lifetime contract: the original request's spans alias the transport frame and die with the handler, and the original struct itself is recycled, so any retention must go through Clone.
func (*Request) ID ¶ added in v1.0.0
ID returns the request id. It is the zero ID for a notification.
func (*Request) Params ¶ added in v0.7.0
func (r *Request) Params() RawMessage
Params returns the request parameters, nil when absent or null.
type RequestMessage ¶ added in v1.0.0
type RequestMessage interface {
Message
// Method reports the name of the method to invoke.
Method() string
// Params reports the raw, already-encoded parameters of the method, or nil
// when the request carries no parameters.
Params() RawMessage
// contains filtered or unexported methods
}
RequestMessage is the shared interface for wire messages that ask a peer to invoke a method. The set of implementations is closed to *Call and *Notification. It is the message-model view used by ParseRequests and batch parsing; handlers receive the concrete Request instead.
type Response ¶ added in v0.7.0
type Response struct {
// contains filtered or unexported fields
}
Response is a reply to a Call. It carries the same ID as the call it answers, and exactly one of a result or an error.
func NewResponse ¶ added in v0.7.0
func NewResponse(id ID, result RawMessage, err error) *Response
NewResponse constructs a *Response for the supplied id, pre-encoded result, and error. When err is non-nil the result is ignored on the wire.
func (*Response) Result ¶ added in v0.7.0
func (r *Response) Result() RawMessage
Result reports the raw, already-encoded result of the response, or nil when the response carries an error.
type Server ¶ added in v1.0.0
type Server = Conn
Server is the server-oriented runtime mode.
It currently uses Conn because the existing connection already provides the server read loop, preemption, async handler release, and batch dispatch. The separate name keeps server-only call sites from depending on client-mode constructors.
type ServerFunc ¶ added in v0.7.0
ServerFunc is an adapter that implements the StreamServer interface using an ordinary function.
func (ServerFunc) ServeStream ¶ added in v0.7.0
func (f ServerFunc) ServeStream(ctx context.Context, c Conn) error
ServeStream implements StreamServer by calling f(ctx, c).
type SingleClient ¶ added in v1.0.0
type SingleClient = SyncClient
SingleClient is the single-flight, caller-owned-read-loop client mode.
It is an alias of SyncClient so the current low-latency synchronous client becomes the named baseline for the mode split without adding another runtime layer.
func NewSingleClient ¶ added in v1.0.0
func NewSingleClient(stream Stream, opts ...Option) (*SingleClient, error)
NewSingleClient creates a SingleClient over stream.
type Stream ¶
type Stream interface {
// Read decodes the next message from the stream. It returns [io.EOF] when
// the peer closes the connection at a clean frame boundary, and
// [io.ErrUnexpectedEOF] when the connection ends in the middle of a frame.
//
// A decoded request's method and params borrow the stream's read buffer
// and are valid only until the next Read; copy what you retain. Responses
// own their bytes.
Read(ctx context.Context) (Message, int64, error)
// Write frames msg and writes it to the stream as a single contiguous write.
Write(ctx context.Context, msg Message) (int64, error)
// Close closes the underlying connection.
Close() error
}
Stream is a bidirectional channel of JSON-RPC messages over a single connection. A Stream adapts a byte transport (the framing) to the message boundary: Read decodes the next framed Message and Write frames and emits one.
The int64 returned by Read and Write is the number of wire bytes transferred for that message, including any framing overhead (the LSP header, or the newline delimiter for ndjson). It is informational and is intended for transport accounting; a zero count accompanies an error.
A Stream owns the underlying connection: Close closes it. Read is expected to be driven from a single goroutine (the connection's read loop). Write is safe for concurrent use; concurrent writers are serialized so that each message is emitted as one contiguous frame.
func NewChannelStreamPair ¶ added in v1.0.0
NewChannelStreamPair returns two connected in-memory streams backed by bounded channels of encoded JSON-RPC frames.
The capacity controls the number of frames each direction can buffer before a writer blocks. A capacity of zero gives rendezvous semantics. Negative capacities panic, matching make(chan T, capacity).
This stream pair is for same-process peers that need an in-memory JSON-RPC transport; it is not a replacement for network or stdio transports.
Frames sent with WriteFrame are copied before they are queued, so the caller may reuse or mutate its buffer immediately after WriteFrame returns. A frame returned by ReadFrame is valid only until the next read on the same stream: delivered frames are recycled through a frame pool, which is what lets steady-state traffic queue frames without copying or allocating. Each stream supports one reader at a time, matching the package's connection read loop. Closing either stream closes the pair, makes later reads and writes fail with io.EOF, and unblocks pending reads and writes with io.EOF.
func NewHeaderStream ¶ added in v1.0.0
func NewHeaderStream(conn io.ReadWriteCloser) Stream
NewHeaderStream returns a Stream over conn using the LSP header framing:
Content-Length: <n>\r\n [Content-Type: ...\r\n] \r\n <n bytes of JSON>
Unknown header fields are ignored. A frame with a missing, zero, or negative Content-Length is rejected. The header and body are composed into a single pooled buffer and emitted with one Write per message.
func NewNDJSONStream ¶ added in v1.0.0
func NewNDJSONStream(conn io.ReadWriteCloser) Stream
NewNDJSONStream returns a Stream over conn using newline-delimited JSON framing: each message is one JSON value followed by a single '\n', and the payload plus its delimiter are written with one Write per message.
The framing assumes compact payloads: a literal newline inside a message would be read as a frame boundary. The wire encoder (EncodeMessage) escapes control characters, so the strings it controls (method names, string identifiers, error messages) never embed a raw newline; callers that supply pre-encoded params or result bytes must keep them newline-free, which compact JSON always is.
func NewRawStream ¶ added in v0.7.0
func NewRawStream(conn io.ReadWriteCloser) Stream
NewRawStream returns a Stream over conn using newline-delimited JSON (ndjson) framing: each message is one JSON value followed by a single '\n'. This framing is compatible with the Model Context Protocol stdio transport.
NewRawStream is an alias for NewNDJSONStream; it preserves the name used by the gopls-derived API.
func NewStream ¶
func NewStream(conn io.ReadWriteCloser) Stream
NewStream returns a Stream over conn using the LSP base-protocol framing (an HTTP-like header carrying Content-Length, a blank line, then the JSON body). It is the framing used by the Language Server Protocol.
NewStream is an alias for NewHeaderStream; the latter name states the framing explicitly.
type StreamServer ¶ added in v0.7.0
StreamServer is used to serve incoming jsonrpc2 clients communicating over a newly created connection.
ServeStream is called once per accepted connection, on its own goroutine, with a Conn wrapping that connection. It should drive the connection (typically by calling Conn.Go and waiting on Conn.Done) and return when the connection is finished. The connection's stream is closed by Serve after ServeStream returns.
func HandlerServer ¶ added in v0.7.0
func HandlerServer(h Handler) StreamServer
HandlerServer returns a StreamServer that serves each incoming connection by dispatching its requests to h.
For each connection it starts the read goroutine with Conn.Go, waits for the connection to terminate with Conn.Done, and reports Conn.Err.
type SyncClient ¶ added in v1.0.0
type SyncClient struct {
// contains filtered or unexported fields
}
SyncClient is a synchronous JSON-RPC 2.0 client that owns its read loop: it never starts a background read goroutine. Each SyncClient.Call writes the request and then reads frames on the caller's own goroutine until the matching response arrives, so a round trip collapses the dedicated-reader-to-caller hand-off that a Conn pays (the third goroutine hop). On an in-process transport this is the lowest-latency request path the package offers.
The tradeoff is the reason it is a distinct type rather than the default Call path: a SyncClient cannot asynchronously receive server-initiated requests (there is no background reader to dispatch them), and its calls are serialized — exactly one call may be outstanding at a time, guarded by an internal mutex. A peer that issues server-to-client calls, or a caller that needs concurrent in-flight calls on one connection, must use Conn with Conn.Go. SyncClient is for the common client-drives-server request/reply shape over an in-process or point-to-point transport.
A SyncClient requires a Stream that also exposes raw frame access (the built-in NewNDJSONStream and NewHeaderStream framers do); a plain Stream without frame access is rejected by NewSyncClient.
func NewSyncClient ¶ added in v1.0.0
func NewSyncClient(stream Stream, opts ...Option) (*SyncClient, error)
NewSyncClient creates a SyncClient over stream. It returns an error if stream does not expose raw frame access (the built-in framers do). The codec defaults to DefaultCodec; pass WithCodec via opts to override it.
func (*SyncClient) Call ¶ added in v1.0.0
Call invokes method on the peer and blocks on the caller's goroutine until the matching response arrives, decoding its result into result (which may be nil to discard it). params is marshaled with the client's codec; a nil params sends no parameters.
Calls are serialized: a second Call blocks until the first returns. Frames that arrive before the matching response (a notification or a response to an abandoned call) are skipped.
func (*SyncClient) Close ¶ added in v1.0.0
func (c *SyncClient) Close() error
Close closes the underlying stream. After Close, further calls return ErrClientClosing.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package conformance holds JSON-RPC 2.0 wire vectors shared across the jsonrpc2 test suite.
|
Package conformance holds JSON-RPC 2.0 wire vectors shared across the jsonrpc2 test suite. |
|
examples
|
|
|
batch
command
Command batch demonstrates sending one raw JSON-RPC batch frame.
|
Command batch demonstrates sending one raw JSON-RPC batch frame. |
|
peer
command
Command peer demonstrates a bidirectional in-process JSON-RPC connection.
|
Command peer demonstrates a bidirectional in-process JSON-RPC connection. |
|
serve
command
Command serve demonstrates serving JSON-RPC requests over a loopback listener.
|
Command serve demonstrates serving JSON-RPC requests over a loopback listener. |