jrpc2

package module
v0.0.0-...-4fe706f Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2023 License: MIT Imports: 12 Imported by: 5

README

Golang JSON-RPC 2.0 HTTP Server

GoDoc

This library is an HTTP server implementation of the JSON-RPC 2.0 Specification. The library is fully spec compliant with support for named and positional arguments and batch requests.

Installation
go get github.com/bitwurx/jrpc2
Quickstart
package main

import (
    "encoding/json"
    "errors"
    "os"

    "github.com/bitwurx/jrpc2"
)

// This struct is used for unmarshaling the method params
type AddParams struct {
    X *float64 `json:"x"`
    Y *float64 `json:"y"`
}

// Each params struct must implement the FromPositional method.
// This method will be passed an array of interfaces if positional parameters
// are passed in the rpc call
func (ap *AddParams) FromPositional(params []interface{}) error {
    if len(params) != 2 {
        return errors.New("exactly two integers are required")
    }

    x := params[0].(float64)
    y := params[1].(float64)
    ap.X = &x
    ap.Y = &y

    return nil
}

// Each method should match the prototype <fn(json.RawMessage) (inteface{}, *ErrorObject)>
func Add(params json.RawMessage) (interface{}, *jrpc2.ErrorObject) {
    p := new(AddParams)

    // ParseParams is a helper function that automatically invokes the FromPositional
    // method on the params instance if required
    if err := jrpc2.ParseParams(params, p); err != nil {
        return nil, err
    }

    if p.X == nil || p.Y == nil {
        return nil, &jrpc2.ErrorObject{
            Code:    jrpc2.InvalidParamsCode,
            Message: jrpc2.InvalidParamsMsg,
            Data:    "exactly two integers are required",
        }
    }

    return *p.X + *p.Y, nil
}

func main() {
    // create a new server instance
    s := jrpc2.NewServer(":8888", "/api/v1/rpc", nil)

    // register the add method
    s.Register("add", jrpc2.Method{Method: Add})

    // register the subtract method to proxy another rpc server
    // s.Register("add", jrpc2.Method{Url: "http://localhost:9999/api/v1/rpc"})

    // start the server instance
    s.Start()
}

When defining your own registered methods with the rpc server it is important to consider both named and positional parameters per the specification.

While named arguments are more straightforward, this library aims to be fully spec compliant, therefore positional parameters must be handled accordingly.

The ParseParams helper function should be used to ensure positional parameters are automatically resolved by the params struct's FromPositional handler method. The spec states by-position: params MUST be an Array, containing the values in the Server expected order., so handling positional argument by direct subscript reference, where positional arguments are valid, should be considered safe.

Multiplexing Server

The jrpc2 Server only supports a single method handler. This may not be suitable for versioned rpc APIs or any other implementation that requires more than a single rpc route. The multiplexing server was added to support this use case.

Example:

package main

import (
    "encoding/json"
    "github.com/bitwurx/jrpc2"
)

type AddV1Params struct {
    X int `json:x`
    Y int `json:y`
}

func (p *AddV1Params) FromPositional(params []interface{}) error {
    p.X = int(params[0].(float64))
    p.Y = int(params[1].(float64))
    return nil
}

func AddV1(params json.RawMessage) (interface{}, *jrpc2.ErrorObject) {
    p := new(AddV1Params)
    if err := jrpc2.ParseParams(params, p); err != nil {
        return nil, err
    }
    return p.X + p.Y, nil
}

type AddV2Params struct {
    Args []float64 `json:args`
}

func (p *AddV2Params) FromPositional(params []interface{}) error {
    p.Args = params[0].([]float64)
    return nil
}

func AddV2(params json.RawMessage) (interface{}, *jrpc2.ErrorObject) {
    p := new(AddV2Params)
    if err := jrpc2.ParseParams(params, p); err != nil {
        return nil, err
    }
    return p.Args[0] + p.Args[1], nil
}

func main() {
    v1 := jrpc2.NewMuxHandler()
    v1.Register("add", jrpc2.Method{Method: AddV1})
    v2 := jrpc2.NewMuxHandler()
    v2.Register("add", jrpc2.Method{Method: AddV2})
    s := jrpc2.NewMuxServer(":8080", nil)
    s.AddHandler("/rpc/v1", v1)
    s.AddHandler("/rpc/v2", v2)
    s.Start()
}

The mux server api is designed to mimic the single server api as closely as possible. The key difference is the addition of the mux handler which handles method registration. Once methods are registered to the handler, the handler is added to the mux server. The mux server can then be started with the Start() method exactly like the single server.

Each registered handler isolates all registered methods so duplicating method names between handlers is fully supported.

Warning: Mixing single and multiplexing servers can result in unexpected behavior and is not recommended.

Proxy Server

The jrpc2 HTTP server is capable of proxying another jrpc2 HTTP server's requests out of the box. The jrpc2.register method allows rpc registration of a method. Registration requires a method name and a url of the server to proxy.

The following request is an example of method registration:

{"jsonrpc": "2.0", "method": "jrpc2.register", "params": ["subtract", "http://localhost:8080/api/v1/rpc"]}

Methods can also be explicitly registered using the server's Register method:

s.Register("add", jrpc2.Method{Url: "http://localhost:8080/api/v1/rpc"})

Stopping the Server

The server can be stopped by calling the Shutdown method. The Shutdown method accepts a context and a timeout.
The timeout is used to limit the amount of time the server will wait for active connections to close. If the timeout is reached, the server will forcefully close all active connections. If a timeout of 0 is provided, the behavior is dependent on the passed in context.

Example:

package main

import (
	"context"
	"encoding/json"
	"log"
	"time"
	"github.com/bitwurx/jrpc2"
)

func Ping(_ json.RawMessage) (interface{}, *jrpc2.ErrorObject) {
	return "pong", nil
}

func main() {
	s := jrpc2.NewServer(":8888", "/api/v1/rpc", nil)
	s.Register("ping", jrpc2.Method{Method: Ping})
	go s.Start()

	// do other stuff

	// Gracefully stop the server 
	ctx := context.Background()
	if err := s.Shutdown(ctx, 5*time.Second); err != nil {
		log.Fatalf("%v", err)
	}
}
Explicit Server Lifecycle Management

Usually it's enough to call the various .Start*() methods (and optionally .Shutdown()) to get started. In more complex applications you might want to have more direct control over the lifecycle of the underlying http server.

This is possible by using the .Prepare() (or .PrepareWithMiddleware()) methods instead of calling one of the .Start*() methods. It will return the http.Server instance used internally and allow you to fully control it.

Example:

package main

import (
    "context"
    "encoding/json"
    "log"
    "time"
    "github.com/bitwurx/jrpc2"
)

func Ping(_ json.RawMessage) (interface{}, *jrpc2.ErrorObject) {
    return "pong", nil
}

func main() {
    s := jrpc2.NewServer(":8888", "/api/v1/rpc", nil)
    s.Register("ping", jrpc2.Method{Method: Ping})
    
    httpServer := s.Prepare()
    go httpServer.ListenAndServe()

    // do other stuff

    // Gracefully stop the server 
    ctx := context.Background()
    if err := httpServer.Shutdown(ctx, 5*time.Second); err != nil {
        log.Fatalf("%v", err)
    }
}
Running Tests

This library contains a set of api tests to verify spec compliance. The provided tests are a subset of the Section 7 Examples here.

go test ./... -v
License
Copyright (c) 2017 Jared Patrick <jared.patrick@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewResponse

func NewResponse(result interface{}, errObj *ErrorObject, id interface{}, nl bool) []byte

NewResponse creates a bytes encoded representation of a response. Both result and error response objects can be created. The nl flag specifies if the response should be newline terminated.

Types

type Batch

type Batch struct {
	// Responses contains the byte representations of a batch of responses.
	Responses [][]byte
}

Batch is a wrapper around multiple response objects.

func (*Batch) AddResponse

func (b *Batch) AddResponse(resp []byte)

AddResponse inserts the response into the batch responses.

func (*Batch) MakeResponse

func (b *Batch) MakeResponse() []byte

MakeResponse creates a bytes encoded representation of a response object.

type ErrorCode

type ErrorCode int

ErrorCode is a json rpc 2.0 error code.

const (
	ParseErrorCode     ErrorCode = -32700
	InvalidRequestCode ErrorCode = -32600
	MethodNotFoundCode ErrorCode = -32601
	InvalidParamsCode  ErrorCode = -32602
	InternalErrorCode  ErrorCode = -32603
	MethodExistsCode   ErrorCode = -32000
	URLSchemeErrorCode ErrorCode = -32001
)

Error codes

type ErrorMsg

type ErrorMsg string

ErrorMsg is a json rpc 2.0 error message.

const (
	ParseErrorMsg     ErrorMsg = "Parse error"
	InvalidRequestMsg ErrorMsg = "Invalid Request"
	MethodNotFoundMsg ErrorMsg = "Method not found"
	InvalidParamsMsg  ErrorMsg = "Invalid params"
	InternalErrorMsg  ErrorMsg = "Internal error"
	ServerErrorMsg    ErrorMsg = "Server error"
	MethodExistsMsg   ErrorMsg = "Method exists"
	URLSchemeErrorMsg ErrorMsg = "URL scheme error"
)

Error message

type ErrorObject

type ErrorObject struct {
	// Code indicates the error type that occurred.
	// Message provides a short description of the error.
	// Data is a primitive or structured value that contains additional information.
	// about the error.
	Code    ErrorCode   `json:"code"`
	Message ErrorMsg    `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

ErrorObject represents a response error object.

func ParseParams

func ParseParams(params json.RawMessage, p Params) *ErrorObject

ParseParams processes the params data structure from the request. Named parameters will be umarshaled into the provided Params inteface. Positional arguments will be passed to Params interface's FromPositional method for extraction.

type Method

type Method struct {
	// Url is the url of the server that handles the method.
	// Method is the callable function
	Url    string
	Method func(params json.RawMessage) (interface{}, *ErrorObject)
}

Method represents an rpc method.

type MethodWithContext

type MethodWithContext struct {
	// Url is the url of the server that handles the method.
	// Method is the callable function
	Url    string
	Method func(ctx context.Context, params json.RawMessage) (interface{}, *ErrorObject)
}

MethodWithContext represents an rpc method with a context.

type MuxHandler

type MuxHandler struct {
	Methods map[string]MethodWithContext
}

MuxHandler is a method dispatcher that handles request at a designated route.

func NewMuxHandler

func NewMuxHandler() *MuxHandler

NewMuxHandler creates a new mux handler instance.

func (*MuxHandler) Register

func (h *MuxHandler) Register(name string, method Method)

Register adds the method to the handler methods.

func (*MuxHandler) RegisterWithContext

func (h *MuxHandler) RegisterWithContext(name string, method MethodWithContext)

RegisterWithContext adds the method to the handler methods.

type MuxServer

type MuxServer struct {
	Host     string
	Headers  map[string]string
	Handlers map[string]*MuxHandler
	// contains filtered or unexported fields
}

MuxServer is a json rpc 2 server that handles multiple requests.

func NewMuxServer

func NewMuxServer(host string, headers map[string]string) *MuxServer

NewMuxServer creates a new mux handler instance.

func (*MuxServer) AddHandler

func (s *MuxServer) AddHandler(route string, handler *MuxHandler)

AddHandler add the handler to the mux handlers.

func (*MuxServer) Prepare

func (s *MuxServer) Prepare() *http.Server

Prepare

func (*MuxServer) Shutdown

func (s *MuxServer) Shutdown(ctx context.Context, timeout time.Duration) error

Shutdown stops the server from accepting new requests and shuts down the server. If timeout is not 0, the given context is wrapped in a new context with the given timeout.

func (*MuxServer) Start

func (s *MuxServer) Start()

Start Starts binds all server rpcHandlers to their handler routes and starts the http server.

func (*MuxServer) StartTLS

func (s *MuxServer) StartTLS(certFile, keyFile string)

StartTLS Starts binds all server rpcHandlers to their handler routes and starts the https server.

type Params

type Params interface {
	FromPositional([]interface{}) error
}

Params defines methods for processing request parameters.

type RegisterRPCParams

type RegisterRPCParams struct {
	// Name is the the name of the method being registered.
	// Url is the url of the server that handles the method.
	Name *string
	Url  *string
}

RegisterRPCParams is a paramater spec for the RegisterRPC method.

func (*RegisterRPCParams) FromPositional

func (rp *RegisterRPCParams) FromPositional(params []interface{}) error

FromPositional extracts the positional name and url parameters from a list of parameters.

type RequestObject

type RequestObject struct {
	// Jsonrpc specifies the version of the JSON-RPC protocol.
	// Must be exactly "2.0".
	// Method contains the name of the method to be invoked.
	// Params is a structured value that holds the parameter values to be used during
	// the invocation of the method.
	// Id is a unique identifier established by the client.
	Jsonrpc string          `json:"jsonrpc"`
	Method  interface{}     `json:"method"`
	Params  json.RawMessage `json:"params"`
	Id      interface{}     `json:"id"`
	// contains filtered or unexported fields
}

RequestObject represents a request object

type ResponseObject

type ResponseObject struct {
	// Jsonrpc specifies the version of the JSON-RPC protocol.
	// Must be exactly "2.0".
	// Error contains the error object if an error occurred while processing the request.
	// Result contains the result of the called method.
	// Id contains the client established request id or null.
	Jsonrpc string       `json:"jsonrpc"`
	Error   *ErrorObject `json:"error,omitempty"`
	Result  interface{}  `json:"result,omitempty"`
	Id      interface{}  `json:"id"`
}

ResponseObject represents a response object.

type Server

type Server struct {
	// Host is the host:port of the server.
	// Route is the path to the rpc api.
	// Methods contains the mapping of registered methods.
	// Headers contains response headers.
	Host    string
	Route   string
	Methods map[string]MethodWithContext
	Headers map[string]string
	// contains filtered or unexported fields
}

Server represents a jsonrpc 2.0 capable web server.

func NewServer

func NewServer(host, route string, headers map[string]string) *Server

NewServer creates a new server instance.

func (*Server) Call

func (s *Server) Call(ctx context.Context, name interface{}, params json.RawMessage) (interface{}, *ErrorObject)

Call invokes the named method with the provided parameters. If a method from the server Methods has a Method member will be called locally. If a method from the server Methods has a Url member it will be called by proxy.

func (*Server) HandleBatch

func (s *Server) HandleBatch(w http.ResponseWriter, reqs []*RequestObject)

HandleBatch validates, calls, and returns the results of a batch of rpc client requests. Batch methods are called in individual goroutines and collected in a single response.

func (*Server) HandleRequest

func (s *Server) HandleRequest(w http.ResponseWriter, req *RequestObject)

HandleRequest validates, calls, and returns the result of a single rpc client request.

func (*Server) ParseRequest

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

ParseRequest parses the json request body and unpacks into one or more. RequestObjects for single or batch processing.

func (*Server) Prepare

func (s *Server) Prepare() *http.Server

Prepare prepares the http.Server instance for accepting requests and returns it but doesn't start it yet.

func (*Server) PrepareWithMiddleware

func (s *Server) PrepareWithMiddleware(m func(next http.HandlerFunc) http.HandlerFunc) *http.Server

PrepareWithMiddleware prepares the http.Server instance for accepting requests and returns it but doesn't start it yet.

func (*Server) Register

func (s *Server) Register(name string, method Method)

Register maps the provided method to the given name for later method calls.

func (*Server) RegisterRPC

func (s *Server) RegisterRPC(ctx context.Context, params json.RawMessage) (interface{}, *ErrorObject)

RegisterRPC accepts a method name and server url to register a proxy rpc method. A method name can be only be registered once.

func (*Server) RegisterWithContext

func (s *Server) RegisterWithContext(name string, method MethodWithContext)

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context, timeout time.Duration) error

Shutdown stops the server from accepting new requests and shuts down the server. If timeout is not 0, the given context is wrapped in a new context with the given timeout.

func (*Server) Start

func (s *Server) Start()

Start binds the rpcHandler to the server route and starts the http server.

func (*Server) StartTLS

func (s *Server) StartTLS(certFile, keyFile string)

StartTLS binds the rpcHandler to the server route and starts the https server.

func (*Server) StartTLSWithMiddleware

func (s *Server) StartTLSWithMiddleware(certFile, keyFile string, m func(next http.HandlerFunc) http.HandlerFunc)

StartTLSWithMiddleware binds the rpcHandler, with its middleware to the server route and starts the https server.

func (*Server) StartWithMiddleware

func (s *Server) StartWithMiddleware(m func(next http.HandlerFunc) http.HandlerFunc)

StartWithMiddleware binds the rpcHandler, with its middleware to the server route and starts the http server.

func (*Server) ValidateRequest

func (s *Server) ValidateRequest(req *RequestObject) *ErrorObject

ValidateRequest validates that the request json contains valid values.

Jump to

Keyboard shortcuts

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