jrpc2

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 1, 2024 License: BSD-3-Clause Imports: 16 Imported by: 82

README

jrpc2

GoDoc

This repository provides a Go module that implements a JSON-RPC 2.0 client and server. There is also a working example in the Go playground.

Packages

  • Package jrpc2 implements the base client and server and standard error codes.

  • Package channel defines the communication channel abstraction used by the server & client.

  • Package handler defines support for adapting functions to service methods.

  • Package jhttp allows clients and servers to use HTTP as a transport.

  • Package server provides support for running a server to handle multiple connections, and an in-memory implementation for testing.

Versioning

From v1.0.0 onward, the API of this module is considered stable, and I intend to merge no breaking changes to the API without increasing the major version number. Following the conventions of semantic versioning, the minor version will be used to signify the presence of backward-compatible new features (for example, new methods, options, or types), while the patch version will be reserved for bug fixes, documentation updates, and other changes that do not modify the API surface.

Note, however, that this intent is limited to the package APIs as seen by the Go compiler: Changes to the implementation that change observable behaviour in ways not promised by the documentation, e.g., changing performance characteristics or the order of internal operations, are not protected. Breakage that results from reliance on undocumented side-effects of the current implementation are the caller's responsibility.

Implementation Notes

The following describes some of the implementation choices made by this module.

Batch requests and error reporting

The JSON-RPC 2.0 spec is ambiguous about the semantics of batch requests. Specifically, the definition of notifications says:

A Notification is a Request object without an "id" member. ... The Server MUST NOT reply to a Notification, including those that are within a batch request.

Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params", "Internal error").

This conflicts with the definition of batch requests, which asserts:

A Response object SHOULD exist for each Request object, except that there SHOULD NOT be any Response objects for notifications. ... The Response objects being returned from a batch call MAY be returned in any order within the Array. ... If the batch rpc call itself fails to be recognized as an valid JSON or as an Array with at least one value, the response from the Server MUST be a single Response object.

and includes examples that contain request values with no ID (which are, perforce, notifications) and report errors back to the client. Since order may not be relied upon, and there are no IDs, the client cannot correctly match such responses back to their originating requests.

This implementation resolves the conflict in favour of the batch rules. Specifically:

  • If a batch is empty or not valid JSON, the server reports error -32700 (Invalid JSON) as a single error Response object.

  • Otherwise, parse or validation errors resulting from any batch member without an ID are mapped to error objects with a null ID, in the same position in the reply as the corresponding request. Preservation of order is not required by the specification, but it ensures the server has stable behaviour.

Because a server is allowed to reorder the results, a client should not depend on this implementation detail.

Documentation

Overview

Package jrpc2 implements a server and a client for the JSON-RPC 2.0 protocol defined by http://www.jsonrpc.org/specification.

Servers

The *Server type implements a JSON-RPC server. A server communicates with a client over a channel.Channel, and dispatches client requests to user-defined method handlers. Method handlers are functions with this signature:

func(ctx Context.Context, req *jrpc2.Request) (any, error)

A server finds the handler for a request by looking up its method name in a jrpc2.Assigner. A Handler decodes request parameters using the UnmarshalParams method on the Request:

func Handle(ctx context.Context, req *jrpc2.Request) (any, error) {
   var args ArgType
   if err := req.UnmarshalParams(&args); err != nil {
      return nil, err
   }
   return usefulStuffWith(args)
}

The handler package uses reflection to adapt functions that do not have this type to handlers. For example, given:

// Add returns the sum of a slice of integers.
func Add(ctx context.Context, values []int) int {
   sum := 0
   for _, v := range values {
      sum += v
   }
   return sum
}

call handler.New to convert Add to a handler function:

h := handler.New(Add)  // h is now a handler.Func that calls Add

The handler package also provides handler.Map, which implements the Assigner interface with a Go map. To advertise this function under the name "Add":

assigner := handler.Map{
   "Add": handler.New(Add),
}

Equipped with an Assigner we can now construct a Server:

srv := jrpc2.NewServer(assigner, nil)  // nil for default options

To start the server, we need a channel.Channel. Implementations of the Channel interface handle the framing, transmission, and receipt of JSON messages. The channel package implements some common framing disciplines for byte streams like pipes and sockets.

For this example, we'll use a channel that communicates over stdin and stdout, with messages delimited by newlines:

ch := channel.Line(os.Stdin, os.Stdout)

Now we can start the server:

srv.Start(ch)

The Start method does not block. A server runs until its channel closes or it is stopped explicitly by calling srv.Stop(). To wait for the server to finish, call:

err := srv.Wait()

This reports the error that led to the server exiting. The code for this example is available from tools/examples/adder/adder.go:

$ go run tools/examples/adder/adder.go

Interact with the server by sending JSON-RPC requests on stdin, such as for example:

{"jsonrpc":"2.0", "id":1, "method":"Add", "params":[1, 3, 5, 7]}

Clients

The *Client type implements a JSON-RPC client. A client communicates with a server over a channel.Channel, and is safe for concurrent use by multiple goroutines. It supports batched requests and may have arbitrarily many pending requests in flight simultaneously.

To create a client we need a channel:

import "net"

conn, err := net.Dial("tcp", "localhost:8080")
...
ch := channel.Line(conn, conn)
cli := jrpc2.NewClient(ch, nil)  // nil for default options

To send a single RPC, use the Call method:

rsp, err := cli.Call(ctx, "Math.Add", []int{1, 3, 5, 7})

Call blocks until the response is received. Errors returned by the server have concrete type *jrpc2.Error.

To issue a batch of concurrent requests, use the Batch method:

rsps, err := cli.Batch(ctx, []jrpc2.Spec{
   {Method: "Math.Add", Params: []int{1, 2, 3}},
   {Method: "Math.Mul", Params: []int{4, 5, 6}},
   {Method: "Math.Max", Params: []int{-1, 5, 3, 0, 1}},
})

Batch blocks until all the responses are received. An error from the Batch call reflects an error in sending the request: The caller must check each response separately for errors from the server. Responses are returned in the same order as the Spec values, save that notifications are omitted.

To decode the result from a successful response, use its UnmarshalResult method:

var result int
if err := rsp.UnmarshalResult(&result); err != nil {
   log.Fatalln("UnmarshalResult:", err)
}

To close a client and discard all its pending work, call cli.Close().

Notifications

A JSON-RPC notification is a one-way request: The client sends the request to the server, but the server does not reply. Use the Notify method of a client to send a notification:

err := cli.Notify(ctx, "Alert", handler.Obj{
   "message": "A fire is burning!",
})

A notification is complete once it has been sent. Notifications can also be sent as part of a batch request:

rsps, err := cli.Batch(ctx, []jrpc2.Spec{
   {Method: "Alert", Params: note, Notify: true},  // this is a notification
   {Method: "Math.Add": Params: []int{1, 2}},      // this is a normal call
   // ...
})

On the server, notifications are handled just like other requests, except that the return value is discarded once the handler returns. If a handler does not want to do anything for a notification, it can query the request:

if req.IsNotification() {
   return 0, nil  // ignore notifications
}

Services with Multiple Methods

The example above shows a server with one method. A handler.Map works for any number of distinctly-named methods:

mathService := handler.Map{
   "Add": handler.New(Add),
   "Mul": handler.New(Mul),
}

Maps may be combined with the handler.ServiceMap type to export multiple services from the same server:

func getStatus(context.Context) string { return "all is well" }

assigner := handler.ServiceMap{
   "Math":   mathService,
   "Status": handler.Map{
     "Get": handler.New(getStatus),
   },
}

This assigner dispatches "Math.Add" and "Math.Mul" to the arithmetic functions, and "Status.Get" to the getStatus function. A ServiceMap splits the method name on the first period ("."), and you may nest ServiceMaps more deeply if you require a more complex hierarchy.

Concurrency

A Server issues concurrent requests to handlers in parallel, up to the limit given by the Concurrency field in ServerOptions.

Two requests (either calls or notifications) are concurrent if they arrive as part of the same batch. In addition, two calls are concurrent if the time intervals between the arrival of the request objects and delivery of the response objects overlap.

The server may issue concurrent requests to their handlers in any order. Non-concurrent requests are processed in order of arrival. Notifications, in particular, can only be concurrent with other requests in the same batch. This ensures a client that sends a notification can be sure its notification will be fully processed before any subsequent calls are issued to their handlers.

These rules imply that the client cannot rely on the execution order of calls that overlap in time: If the caller needs to ensure that call A completes before call B starts, it must wait for A to return before invoking B.

Built-in Methods

Per the JSON-RPC 2.0 spec, method names beginning with "rpc." are reserved by the implementation. By default, a server does not dispatch these methods to its assigner. In this configuration, the server exports a "rpc.serverInfo" method taking no parameters and returning a jrpc2.ServerInfo value.

Setting the DisableBuiltin server option to true removes special treatment of "rpc." method names, and disables the rpc.serverInfo handler. When this option is true, method names beginning with "rpc." will be dispatched to the assigner like any other method.

Server Push

The AllowPush server option allows handlers to "push" requests back to the client. This is a non-standard extension of JSON-RPC used by some applications such as the Language Server Protocol (LSP). When this option is enabled, the server's Notify and Callback methods send requests back to the client. Otherwise, those methods will report an error:

if err := s.Notify(ctx, "methodName", params); err == jrpc2.ErrPushUnsupported {
  // server push is not enabled
}
if rsp, err := s.Callback(ctx, "methodName", params); err == jrpc2.ErrPushUnsupported {
  // server push is not enabled
}

A method handler may use jrpc2.ServerFromContext to access the server from its context, and then invoke these methods on it. On the client side, the OnNotify and OnCallback options in jrpc2.ClientOptions provide hooks to which any server requests are delivered, if they are set.

Since not all clients support server push, handlers should set a timeout when using the server Callback method; otherwise the callback may block forever for a client response that will never arrive.

Contexts and Cancellation

Both the Server and the Client use the standard context package to plumb cancellation and other request-specific metadata in handlers and calls.

On the server, the context passed to a handler is automatically cancelled when the server shuts down or when the server's CancelRequest method is invoked for that request. In addition, the NewContext server option can be used to supply default timeouts or other context metadata.

On the client, the context passed to the Call and CallResult methods governs the lifetime of the call. If the context ends before the call is complete, the client will terminate the call and report an error.

Note that cancellation on the client is not automatically propagated to the server, as JSON-RPC does not define a standard mechanism to do so. One typical approach (used by LSP, for example) is to define a separate method on the server to handle cancellation requests.

If an OnCancel hook is set in the ClientOptions, the client calls it when the context for a Call ends before the server has responded. This can be used to forward cancellation to the server separately.

Index

Examples

Constants

View Source
const Version = "2.0"

Version is the version string for the JSON-RPC protocol understood by this implementation, defined at http://www.jsonrpc.org/specification.

Variables

View Source
var ErrConnClosed = errors.New("client connection is closed")

ErrConnClosed is returned by a server's push-to-client methods if they are called after the client connection is closed.

View Source
var ErrPushUnsupported = errors.New("server push is not enabled")

ErrPushUnsupported is returned by the Notify and Call methods if server pushes are not enabled.

Functions

func Network added in v0.2.2

func Network(s string) (network, address string)

Network guesses a network type for the specified address and returns a tuple of that type and the address.

The assignment of a network type uses the following heuristics:

If s does not have the form [host]:port, the network is assigned as "unix". The network "unix" is also assigned if port == "", port contains characters other than ASCII letters, digits, and "-", or if host contains a "/".

Otherwise, the network is assigned as "tcp". Note that this function does not verify whether the address is lexically valid.

func ServerMetrics

func ServerMetrics() *expvar.Map

ServerMetrics returns a map of exported server metrics for use with the expvar package. This map is shared among all server instances created by NewServer. The caller is free to add or remove metrics in the map, but note that such changes will affect all servers.

The caller is responsible for publishing the metrics to the exporter via expvar.Publish or similar.

func StrictFields added in v0.11.0

func StrictFields(v any) any

StrictFields wraps a value v to require unknown fields to be rejected when unmarshaling from JSON.

For example:

var obj RequestType
err := req.UnmarshalParams(jrpc2.StrictFields(&obj))`

Types

type Assigner

type Assigner interface {
	// Assign returns the handler for the named method, or returns nil to
	// indicate that the method is not known.
	//
	// The implementation can obtain the complete request from ctx using the
	// jrpc2.InboundRequest function.
	Assign(ctx context.Context, method string) Handler
}

An Assigner maps method names to Handler functions.

type Client

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

A Client is a JSON-RPC 2.0 client. The client sends requests and receives responses on a channel.Channel provided by the constructor.

func ClientFromContext added in v0.31.0

func ClientFromContext(ctx context.Context) *Client

ClientFromContext returns the client associated with the given context. This will be populated on the context passed by a *jrpc2.Client to a client-side callback handler.

A callback handler MUST NOT close the client, as the close will deadlock waiting for the callback to return.

func NewClient

func NewClient(ch channel.Channel, opts *ClientOptions) *Client

NewClient returns a new client that communicates with the server via ch.

func (*Client) Batch

func (c *Client) Batch(ctx context.Context, specs []Spec) ([]*Response, error)

Batch initiates a batch of concurrent requests, and blocks until all the responses return or ctx ends. The responses are returned in the same order as the original specs, omitting notifications.

Any error reported by Batch represents an error in encoding or sending the batch to the server. Errors reported by the server in response to requests must be recovered from the responses.

Example
package main

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

	"github.com/creachadair/jrpc2"
	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
)

var ctx = context.Background()

type Msg struct {
	Text string `json:"msg"`
}

var local = server.NewLocal(handler.Map{
	"Hello": handler.New(func(ctx context.Context) string {
		return "Hello, world!"
	}),
	"Echo": handler.New(func(_ context.Context, args []json.RawMessage) []json.RawMessage {
		return args
	}),
	"Log": handler.New(func(ctx context.Context, msg Msg) (bool, error) {
		fmt.Println("Log:", msg.Text)
		return true, nil
	}),
}, nil)

func main() {
	rsps, err := local.Client.Batch(ctx, []jrpc2.Spec{
		{Method: "Hello"},
		{Method: "Log", Params: Msg{"Sing it!"}, Notify: true},
	})
	if err != nil {
		log.Fatalf("Batch: %v", err)
	}

	fmt.Printf("len(rsps) = %d\n", len(rsps))
	for i, rsp := range rsps {
		var msg string
		if err := rsp.UnmarshalResult(&msg); err != nil {
			log.Fatalf("Invalid result: %v", err)
		}
		fmt.Printf("Response #%d: %s\n", i+1, msg)
	}
}
Output:

Log: Sing it!
len(rsps) = 1
Response #1: Hello, world!

func (*Client) Call

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

Call initiates a single request and blocks until the response returns or ctx ends. A successful call reports a nil error and a non-nil response. Errors from the server have concrete type *jrpc2.Error.

rsp, err := c.Call(ctx, method, params)
if e, ok := err.(*jrpc2.Error); ok {
   log.Fatalf("Error from server: %v", err)
} else if err != nil {
   log.Fatalf("Call failed: %v", err)
}
handleValidResponse(rsp)
Example
package main

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

	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
)

var ctx = context.Background()

type Msg struct {
	Text string `json:"msg"`
}

var local = server.NewLocal(handler.Map{
	"Hello": handler.New(func(ctx context.Context) string {
		return "Hello, world!"
	}),
	"Echo": handler.New(func(_ context.Context, args []json.RawMessage) []json.RawMessage {
		return args
	}),
	"Log": handler.New(func(ctx context.Context, msg Msg) (bool, error) {
		fmt.Println("Log:", msg.Text)
		return true, nil
	}),
}, nil)

func main() {
	rsp, err := local.Client.Call(ctx, "Hello", nil)
	if err != nil {
		log.Fatalf("Call: %v", err)
	}
	var msg string
	if err := rsp.UnmarshalResult(&msg); err != nil {
		log.Fatalf("Decoding result: %v", err)
	}
	fmt.Println(msg)
}
Output:

Hello, world!

func (*Client) CallResult

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

CallResult invokes Call with the given method and params. If it succeeds, the result is decoded into result. This is a convenient shorthand for Call followed by UnmarshalResult. It will panic if result == nil.

Example
package main

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

	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
)

var ctx = context.Background()

type Msg struct {
	Text string `json:"msg"`
}

var local = server.NewLocal(handler.Map{
	"Hello": handler.New(func(ctx context.Context) string {
		return "Hello, world!"
	}),
	"Echo": handler.New(func(_ context.Context, args []json.RawMessage) []json.RawMessage {
		return args
	}),
	"Log": handler.New(func(ctx context.Context, msg Msg) (bool, error) {
		fmt.Println("Log:", msg.Text)
		return true, nil
	}),
}, nil)

func main() {
	var msg string
	if err := local.Client.CallResult(ctx, "Hello", nil, &msg); err != nil {
		log.Fatalf("CallResult: %v", err)
	}
	fmt.Println(msg)
}
Output:

Hello, world!

func (*Client) Close

func (c *Client) Close() error

Close shuts down the client, terminating any pending in-flight requests.

func (*Client) IsStopped added in v1.1.0

func (c *Client) IsStopped() bool

IsStopped reports whether the client has been stopped, either by a call to its Close method or by a failure of the channel to the server.

func (*Client) Notify

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

Notify transmits a notification to the specified method and parameters. It blocks until the notification has been sent or ctx ends.

type ClientOptions

type ClientOptions struct {
	// If not nil, send debug text logs here.
	Logger Logger

	// If set, this function is called if a notification is received from the
	// server. If unset, server notifications are logged and discarded.  At
	// most one invocation of the callback will be active at a time.
	// Server notifications are a non-standard extension of JSON-RPC.
	OnNotify func(*Request)

	// If set, this function is called if a request is received from the server.
	// If unset, server requests are logged and discarded. Multiple invocations
	// of the callback handler may be active concurrently.
	//
	// The callback handler can retrieve the client from its context using the
	// jrpc2.ClientFromContext function. The context terminates when the client
	// is closed.
	//
	// If a callback handler panics, the client will recover the panic and
	// report a system error back to the server describing the error.
	//
	// Server callbacks are a non-standard extension of JSON-RPC.
	OnCallback Handler

	// If set, this function is called when the context for a request terminates.
	// The function receives the client and the response that was cancelled.
	// The hook can obtain the ID and error value from rsp.
	//
	// Note that the hook does not receive the request context, which has
	// already ended by the time the hook is called.
	OnCancel func(cli *Client, rsp *Response)

	// If set, this function is called when the client is stopped, either by
	// calling its Close method or by disconnection of its channel.  The
	// arguments are the client itself and the error that caused it to stop.
	OnStop func(cli *Client, err error)
}

ClientOptions control the behaviour of a client created by NewClient. A nil *ClientOptions is valid and provides sensible defaults.

type Code added in v0.46.0

type Code int32

A Code is an error code included in the JSON-RPC error object.

Code values from and including -32768 to -32000 are reserved for predefined JSON-RPC errors. Any code within this range, but not defined explicitly below is reserved for future use. The remainder of the space is available for application defined errors.

See also: https://www.jsonrpc.org/specification#error_object

const (
	ParseError     Code = -32700 // [std] Invalid JSON received by the server
	InvalidRequest Code = -32600 // [std] The JSON sent is not a valid request object
	MethodNotFound Code = -32601 // [std] The method does not exist or is unavailable
	InvalidParams  Code = -32602 // [std] Invalid method parameters
	InternalError  Code = -32603 // [std] Internal JSON-RPC error

	NoError          Code = -32099 // Denotes a nil error (used by ErrorCode)
	SystemError      Code = -32098 // Errors from the operating environment
	Cancelled        Code = -32097 // Request cancelled (context.Canceled)
	DeadlineExceeded Code = -32096 // Request deadline exceeded (context.DeadlineExceeded)
)

Error codes from and including -32768 to -32000 are reserved for pre-defined errors by the JSON-RPC specification. These constants cover the standard codes and implementation-specific codes used by the jrpc2 module.

func ErrorCode added in v0.46.0

func ErrorCode(err error) Code

ErrorCode returns a Code to categorize the specified error.

  • If err == nil, it returns jrpc2.NoError.
  • If err is (or wraps) an ErrCoder, it returns the reported code value.
  • If err is context.Canceled, it returns jrpc2.Cancelled.
  • If err is context.DeadlineExceeded, it returns jrpc2.DeadlineExceeded.
  • Otherwise it returns jrpc2.SystemError.

func (Code) Err added in v0.46.0

func (c Code) Err() error

Err converts c to an error value, which is nil for NoError and otherwise an error value whose code is c and whose text is based on the built-in string for c if one exists.

func (Code) String added in v0.46.0

func (c Code) String() string

type ErrCoder added in v0.46.0

type ErrCoder interface {
	ErrCode() Code
}

An ErrCoder is a value that can report an error code value.

type Error

type Error struct {
	Code    Code            `json:"code"`              // the machine-readable error code
	Message string          `json:"message,omitempty"` // the human-readable error message
	Data    json.RawMessage `json:"data,omitempty"`    // optional ancillary error data
}

Error is the concrete type of errors returned from RPC calls. It also represents the JSON encoding of the JSON-RPC error object.

func Errorf

func Errorf(code Code, msg string, args ...any) *Error

Errorf returns an error value of concrete type *Error having the specified code and formatted message string.

func (Error) ErrCode added in v0.17.0

func (e Error) ErrCode() Code

ErrCode trivially satisfies the ErrCoder interface for an *Error.

func (Error) Error

func (e Error) Error() string

Error returns a human-readable description of e.

func (*Error) WithData added in v0.28.0

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

WithData marshals v as JSON and constructs a copy of e whose Data field includes the result. If v == nil or if marshaling v fails, e is returned without modification.

type Handler

type Handler = func(context.Context, *Request) (any, error)

A Handler function implements a method. The request contains the method name, request ID, and parameters sent by the client. The result value must be JSON-marshalable or nil. In case of error, the handler can return a value of type *jrpc2.Error to control the response code sent back to the caller; otherwise the server will wrap the resulting value.

The context passed to the handler by a *jrpc2.Server includes two special values that the handler may extract.

To obtain the server instance running the handler, write:

srv := jrpc2.ServerFromContext(ctx)

To obtain the inbound request message, write:

req := jrpc2.InboundRequest(ctx)

The latter is primarily useful for wrappers generated by handler.New, which do not receive the request directly.

type Logger added in v0.30.0

type Logger func(text string)

A Logger records text logs from a server or a client. A nil logger discards text log input.

func StdLogger added in v0.30.0

func StdLogger(logger *log.Logger) Logger

StdLogger adapts a *log.Logger to a Logger. If logger == nil, the returned function sends logs to the default logger.

func (Logger) Printf added in v0.30.0

func (lg Logger) Printf(msg string, args ...any)

Printf writes a formatted message to the logger. If lg == nil, the message is discarded.

type Namer added in v0.34.2

type Namer interface {
	// Names returns all known method names in lexicographic order.
	Names() []string
}

Namer is an optional interface that an Assigner may implement to expose the names of its methods to the ServerInfo method.

type ParsedRequest added in v0.36.0

type ParsedRequest struct {
	ID     string
	Method string
	Params json.RawMessage
	Error  *Error
}

A ParsedRequest is the parsed form of a request message. If a request is valid, its Error field is nil. Otherwise, the Error field describes why the request is invalid, and the other fields may be incomplete or missing.

func ParseRequests

func ParseRequests(msg []byte) ([]*ParsedRequest, error)

ParseRequests parses either a single request or a batch of requests from JSON. It reports an error only if msg is not valid JSON. The caller must check the Error field results to determine whether the individual requests are valid.

func (*ParsedRequest) ToRequest added in v0.41.1

func (p *ParsedRequest) ToRequest() *Request

ToRequest converts p to an equivalent server Request. If p.Error is not nil, ToRequest returns nil.

This method does not check validity. If p is from a successful call of ParseRequests, the result will be valid; otherwise the caller must ensure that the ID and parameters are valid JSON.

type RPCLogger added in v0.6.1

type RPCLogger interface {
	// Called for each request received prior to invoking its handler.
	LogRequest(ctx context.Context, req *Request)

	// Called for each response produced by a handler, immediately prior to
	// sending it back to the client. The inbound request can be recovered from
	// the context using jrpc2.InboundRequest.
	LogResponse(ctx context.Context, rsp *Response)
}

An RPCLogger receives callbacks from a server to record the receipt of requests and the delivery of responses. These callbacks are invoked synchronously with the processing of the request.

type Request

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

A Request is a request message from a client to a server.

func InboundRequest

func InboundRequest(ctx context.Context) *Request

InboundRequest returns the inbound request associated with the context passed to a Handler, or nil if ctx does not have an inbound request. A *jrpc2.Server populates this value for handler contexts.

This is mainly useful to wrapped server methods that do not have the request as an explicit parameter; for direct implementations of the Handler type the request value returned by InboundRequest will be the same value as was passed explicitly.

func (*Request) HasParams

func (r *Request) HasParams() bool

HasParams reports whether the request has non-empty parameters.

func (*Request) ID

func (r *Request) ID() string

ID returns the request identifier for r, or "" if r is a notification.

func (*Request) IsNotification

func (r *Request) IsNotification() bool

IsNotification reports whether the request is a notification, and thus does not require a value response.

func (*Request) Method

func (r *Request) Method() string

Method reports the method name for the request.

func (*Request) ParamString added in v0.6.2

func (r *Request) ParamString() string

ParamString returns the encoded request parameters of r as a string. If r has no parameters, it returns "".

func (*Request) UnmarshalParams

func (r *Request) UnmarshalParams(v any) error

UnmarshalParams decodes the request parameters of r into v. If r has empty parameters, it returns nil without modifying v. If the parameters are invalid, UnmarshalParams returns an InvalidParams error.

By default, unknown object keys are ignored and discarded when unmarshaling into a v of struct type. If the type of v implements a DisallowUnknownFields method, unknown fields will instead generate an InvalidParams error. The jrpc2.StrictFields helper adapts existing struct values to this interface. For more specific behaviour, implement a custom json.Unmarshaler.

If v has type *json.RawMessage, unmarshaling will never report an error.

Example
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"

	"github.com/creachadair/jrpc2"
	"github.com/creachadair/jrpc2/internal/testutil"
)

func main() {
	const msg = `{"jsonrpc":"2.0", "id":101, "method":"M", "params":{"a":1, "b":2, "c":3}}`
	req, err := testutil.ParseRequest(msg)
	if err != nil {
		log.Fatalf("Parsing %#q failed: %v", msg, err)
	}

	var t, u struct {
		A int `json:"a"`
		B int `json:"b"`
	}

	// By default, unmarshaling ignores unknown fields (here, "c").
	if err := req.UnmarshalParams(&t); err != nil {
		log.Fatalf("UnmarshalParams: %v", err)
	}
	fmt.Printf("t.A=%d, t.B=%d\n", t.A, t.B)

	// To implement strict field checking, there are several options:
	//
	// Solution 1: Use the jrpc2.StrictFields helper.
	err = req.UnmarshalParams(jrpc2.StrictFields(&t))
	if jrpc2.ErrorCode(err) != jrpc2.InvalidParams {
		log.Fatalf("UnmarshalParams strict: %v", err)
	}

	// Solution 2: Implement a DisallowUnknownFields method.
	var p strictParams
	err = req.UnmarshalParams(&p)
	if jrpc2.ErrorCode(err) != jrpc2.InvalidParams {
		log.Fatalf("UnmarshalParams strict: %v", err)
	}

	// Solution 3: Decode the raw message separately.
	var tmp json.RawMessage
	req.UnmarshalParams(&tmp) // cannot fail
	dec := json.NewDecoder(bytes.NewReader(tmp))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&u); err == nil {
		log.Fatal("Decode should have failed for an unknown field")
	}

}

type strictParams struct {
	A int `json:"a"`
	B int `json:"b"`
}

func (strictParams) DisallowUnknownFields() {}
Output:

t.A=1, t.B=2

type Response

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

A Response is a response message from a server to a client.

func (*Response) Error

func (r *Response) Error() *Error

Error returns a non-nil *Error if the response contains an error.

func (*Response) ID

func (r *Response) ID() string

ID returns the request identifier for r.

func (*Response) MarshalJSON

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

MarshalJSON converts the response to equivalent JSON.

func (*Response) ResultString added in v0.6.2

func (r *Response) ResultString() string

ResultString returns the encoded result value of r as a string. If r has no result, for example if r is an error response, it returns "".

func (*Response) SetID

func (r *Response) SetID(s string)

SetID sets the ID of r to s, for use in proxies.

func (*Response) UnmarshalResult

func (r *Response) UnmarshalResult(v any) error

UnmarshalResult decodes the result message into v. If the request failed, UnmarshalResult returns the same *Error value that is returned by r.Error(), and v is unmodified.

By default, unknown object keys are ignored and discarded when unmarshaling into a v of struct type. If the type of v implements a DisallowUnknownFields method, unknown fields will instead generate an error. The jrpc2.StrictFields helper adapts existing struct values to this interface. For more specific behaviour, implement a custom json.Unmarshaler.

If v has type *json.RawMessage, unmarshaling will never report an error.

Example
package main

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

	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
)

var ctx = context.Background()

type Msg struct {
	Text string `json:"msg"`
}

var local = server.NewLocal(handler.Map{
	"Hello": handler.New(func(ctx context.Context) string {
		return "Hello, world!"
	}),
	"Echo": handler.New(func(_ context.Context, args []json.RawMessage) []json.RawMessage {
		return args
	}),
	"Log": handler.New(func(ctx context.Context, msg Msg) (bool, error) {
		fmt.Println("Log:", msg.Text)
		return true, nil
	}),
}, nil)

func main() {
	rsp, err := local.Client.Call(ctx, "Echo", []string{"alpha", "oscar", "kilo"})
	if err != nil {
		log.Fatalf("Call: %v", err)
	}
	var r1, r3 string

	// Note the nil, which tells the decoder to skip that argument.
	if err := rsp.UnmarshalResult(&handler.Args{&r1, nil, &r3}); err != nil {
		log.Fatalf("Decoding result: %v", err)
	}
	fmt.Println(r1, r3)
}
Output:

alpha kilo

type Server

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

Server implements a JSON-RPC 2.0 server. The server receives requests and sends responses on a channel.Channel provided by the caller, and dispatches requests to user-defined Handlers.

func NewServer

func NewServer(mux Assigner, opts *ServerOptions) *Server

NewServer returns a new unstarted server that will dispatch incoming JSON-RPC requests according to mux. To start serving, call Start.

This function will panic if mux == nil. It is not safe to modify mux after the server has been started unless mux itself is safe for concurrent use by multiple goroutines.

Example
package main

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

	"github.com/creachadair/jrpc2/handler"
	"github.com/creachadair/jrpc2/server"
)

type Msg struct {
	Text string `json:"msg"`
}

var local = server.NewLocal(handler.Map{
	"Hello": handler.New(func(ctx context.Context) string {
		return "Hello, world!"
	}),
	"Echo": handler.New(func(_ context.Context, args []json.RawMessage) []json.RawMessage {
		return args
	}),
	"Log": handler.New(func(ctx context.Context, msg Msg) (bool, error) {
		fmt.Println("Log:", msg.Text)
		return true, nil
	}),
}, nil)

func main() {
	// We can query the server for its current status information, including a
	// list of its methods.
	si := local.Server.ServerInfo()

	fmt.Println(strings.Join(si.Methods, "\n"))
}
Output:

Echo
Hello
Log

func ServerFromContext added in v0.12.0

func ServerFromContext(ctx context.Context) *Server

ServerFromContext returns the server associated with the context passed to a Handler by a *jrpc2.Server. It will panic for a non-handler context.

It is safe to retain the server and invoke its methods beyond the lifetime of the context from which it was extracted; however, a handler must not block on the Wait or WaitStatus methods of the server, as the server will deadlock waiting for the handler to return.

func (*Server) Callback added in v0.10.0

func (s *Server) Callback(ctx context.Context, method string, params any) (*Response, error)

Callback posts a single server-side call to the client. It blocks until a reply is received, ctx ends, or the client connection terminates. A successful callback reports a nil error and a non-nil response. Errors returned by the client have concrete type *jrpc2.Error.

This is a non-standard extension of JSON-RPC, and may not be supported by all clients. If you are not sure whether the client supports push calls, you should set a deadline on ctx, otherwise the callback may block forever for a client response that will never arrive.

Unless s was constructed with the AllowPush option set true, this method will always report an error (ErrPushUnsupported) without sending anything. If Callback is called after the client connection is closed, it returns ErrConnClosed.

func (*Server) CancelRequest added in v0.12.0

func (s *Server) CancelRequest(id string)

CancelRequest instructs s to cancel the pending or in-flight request with the specified ID. If no request exists with that ID, this is a no-op.

func (*Server) Notify added in v0.10.0

func (s *Server) Notify(ctx context.Context, method string, params any) error

Notify posts a single server-side notification to the client.

This is a non-standard extension of JSON-RPC, and may not be supported by all clients. Unless s was constructed with the AllowPush option set true, this method will always report an error (ErrPushUnsupported) without sending anything. If Notify is called after the client connection is closed, it returns ErrConnClosed.

func (*Server) ServerInfo

func (s *Server) ServerInfo() *ServerInfo

ServerInfo returns an atomic snapshot of the current server info for s.

func (*Server) Start

func (s *Server) Start(c channel.Channel) *Server

Start initiates processing of requests from c and returns. Start does not block while the server runs. Start will panic if the server is already running. It returns s to allow chaining with construction.

func (*Server) Stop

func (s *Server) Stop()

Stop shuts down the server. All in-progress call handlers are cancelled. It is safe to call this method multiple times or from concurrent goroutines; it will only take effect once.

func (*Server) Wait

func (s *Server) Wait() error

Wait blocks until the server terminates and returns the resulting error. It is equivalent to s.WaitStatus().Err.

func (*Server) WaitStatus added in v0.6.3

func (s *Server) WaitStatus() ServerStatus

WaitStatus blocks until the server terminates, and returns the resulting status. After WaitStatus returns, whether or not there was an error, it is safe to call s.Start again to restart the server with a fresh channel.

type ServerInfo

type ServerInfo struct {
	// The list of method names exported by this server.
	Methods []string `json:"methods,omitempty"`

	// Metrics defined by the server and handler methods.
	Metrics map[string]any `json:"metrics,omitempty"`

	// When the server started.
	StartTime time.Time `json:"startTime,omitempty"`
}

ServerInfo is the concrete type of responses from the rpc.serverInfo method.

type ServerOptions

type ServerOptions struct {
	// If not nil, send debug text logs here.
	Logger Logger

	// If not nil, the methods of this value are called to log each request
	// received and each response or error returned.
	RPCLog RPCLogger

	// Instructs the server to allow server callbacks and notifications, a
	// non-standard extension to the JSON-RPC protocol. If AllowPush is false,
	// the Notify and Callback methods of the server report errors if called.
	AllowPush bool

	// Instructs the server to disable the built-in rpc.* handler methods.
	//
	// By default, a server reserves all rpc.* methods, even if the given
	// assigner maps them. When this option is true, rpc.* methods are passed
	// along to the given assigner.
	DisableBuiltin bool

	// Allows up to the specified number of goroutines to execute in parallel in
	// request handlers. A value less than 1 uses runtime.NumCPU().  Note that
	// this setting does not constrain order of issue.
	Concurrency int

	// If set, this function is called to create a new base request context.
	// If unset, the server uses a background context.
	NewContext func() context.Context

	// If nonzero this value as the server start time; otherwise, use the
	// current time when Start is called. All servers created from the same
	// options will share the same start time if one is set.
	StartTime time.Time
}

ServerOptions control the behaviour of a server created by NewServer. A nil *ServerOptions is valid and provides sensible defaults. It is safe to share server options among multiple server instances.

type ServerStatus added in v0.6.3

type ServerStatus struct {
	Err error // the error that caused the server to stop (nil on success)

	// On success, these flags explain the reason why the server stopped.
	// At most one of these fields will be true.
	Stopped bool // server exited because Stop was called
	Closed  bool // server exited because the client channel closed
}

ServerStatus describes the status of a stopped server.

A server is said to have succeeded if it stopped because the client channel closed or because its Stop method was called. On success, Err == nil, and the flag fields indicate the reason why the server exited. Otherwise, Err != nil is the error value that caused the server to exit.

func (ServerStatus) Success added in v0.6.3

func (s ServerStatus) Success() bool

Success reports whether the server exited without error.

type Spec

type Spec struct {
	Method string
	Params any
	Notify bool
}

A Spec combines a method name and parameter value as part of a Batch. If the Notify flag is true, the request is sent as a notification.

Directories

Path Synopsis
Package channel defines a communications channel.
Package channel defines a communications channel.
Package handler provides implementations of the jrpc2.Assigner interface, and support for adapting functions to jrpc2.Handler signature.
Package handler provides implementations of the jrpc2.Assigner interface, and support for adapting functions to jrpc2.Handler signature.
internal
testutil
Package testutil defines internal support code for writing tests.
Package testutil defines internal support code for writing tests.
Package jhttp implements a bridge from HTTP to JSON-RPC.
Package jhttp implements a bridge from HTTP to JSON-RPC.
Package server provides support routines for running jrpc2 servers.
Package server provides support routines for running jrpc2 servers.
tools module

Jump to

Keyboard shortcuts

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