jsonrpc2

package module
v4.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2018 License: MIT Imports: 5 Imported by: 0

README

jsonrpc2 - v2.0.1

GoDoc Go Report Card Coverage Status Build Status

Package jsonrpc2 is an easy-to-use, thin, minimalist implementation of the JSON-RPC 2.0 protocol with a handler for HTTP servers. It avoids implementing any HTTP helper functions and instead simply provides conforming Request and Response Types, and an http.HandlerFunc that handles single and batch Requests, protocol errors, and recovers panics from the application's RPC method calls. It strives to conform to the official specification: https://www.jsonrpc.org.

Getting started

Please read the official godoc documentation for the most up to date information.

Quick Overview

Client

Clients can use the Request, Response, and Error types with the json and http packages to make HTTP JSON-RPC 2.0 calls and parse their responses.

Server

Servers must implement their RPC method functions to match the MethodFunc type and then register their function with a name using RegisterMethod(name, function). Read the documentation for RegisterMethod and MethodFunc for more information. RemarshalJSON is a convenience function for converting the abstract params argument into a custom concrete type.

jsonrpc2.RegisterMethod("subtract", func(params interface{}) jsonrpc2.Response {
	var p []interface{}
	var ok bool
	if p, ok = params.([]interface{}); !ok {
                return jsonrpc2.NewInvalidParamsErrorResponse(
                        "params must be an array of two numbers")
	}
	if len(p) != 2 {
                return jsonrpc2.NewInvalidParamsErrorResponse(
                        "params must be an array of two numbers")
	}
	var x [2]float64
	for i := range x {
		if x[i], ok = p[i].(float64); !ok {
                        return jsonrpc2.NewInvalidParamsErrorResponse(
                                "params must be an array of two numbers")
		}
	}
	result := x[0] - x[1]
	return jsonrpc2.NewResponse(result)
})

After all methods are registered, set up an HTTP Server with HTTPRequestHandler as the handler.

http.ListenAndServe(":8080", jsonrpc2.HTTPRequestHandler)

Documentation

Overview

Package jsonrpc2 is a minimalist implementation of the JSON-RPC 2.0 protocol that provides types for Requests and Responses, and an http.Handler that calls MethodFuncs registered with RegisterMethod(). The HTTPRequestHandler will recover from any MethodFunc panics and will always respond with a valid JSON RPC Response, unless of course the request was a notification.

It strives to conform to the official specification: https://www.jsonrpc.org.

Client

Clients can use the Request, Response, and Error types with the json and http packages to make HTTP JSON-RPC 2.0 calls and parse their responses.

reqBytes, _ := json.Marshal(jsonrpc2.NewRequest("subtract", 0, []int{5, 1}))
httpResp, _ := http.Post("www.example.com", "application/json",
        bytes.NewReader(reqBytes))
respBytes, _ := ioutil.ReadAll(httpResp.Body)
response := &jsonrpc2.Response{}
json.Unmarshal(respBytes, response)

Server

Servers must implement their RPC method functions to match the MethodFunc type. Methods must be registered with a name using RegisterMethod().

var func versionMethod(p json.RawMessage) *jsonrpc2.Response {
	if p != nil {
		return jsonrpc2.NewInvalidParamsErrorResponse(nil)
	}
	return jrpc.NewResponse("0.0.0")
}
jsonrpc2.RegisterMethod("version", jsonrpc2.MethodFunc(versionMethod))

Read the documentation for RegisterMethod and MethodFunc for more information.

After all methods are registered, set up an HTTP Server with HTTPRequestHandler as the handler.

http.ListenAndServe(":8080", jsonrpc2.HTTPRequestHandler)
Example

This example makes all of the calls from the examples in the JSON-RPC 2.0 specification and prints them in a similar format.

// github.com/AdamSLevy/jsonrpc2
// Copyright 2018 Adam S Levy. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"

	jrpc "github.com/AdamSLevy/jsonrpc2/v4"
)

var endpoint = "http://localhost:18888"

// Functions for making requests and printing the Requests and Responses.
func post(b []byte) []byte {
	httpResp, _ := http.Post(endpoint, "", bytes.NewReader(b))
	respBytes, _ := ioutil.ReadAll(httpResp.Body)
	return respBytes
}
func postNewRequest(method string, id, params interface{}) {
	postRequest(jrpc.NewRequest(method, id, params))
}
func postRequest(request interface{}) {
	fmt.Println(request)
	reqBytes, _ := json.Marshal(request)
	respBytes := post(reqBytes)
	parseResponse(respBytes)
}
func parseResponse(respBytes []byte) {
	var response interface{}
	if len(respBytes) == 0 {
		return
	} else if string(respBytes[0]) == "[" {
		response = &jrpc.BatchResponse{}
	} else {
		response = &jrpc.Response{}
	}
	json.Unmarshal(respBytes, response)
	fmt.Println(response)
	fmt.Println()
}
func postBytes(req string) {
	fmt.Println("-->", req)
	respBytes := post([]byte(req))
	parseResponse(respBytes)
}

// The RPC methods called in the JSON-RPC 2.0 specification examples.
func subtract(params json.RawMessage) *jrpc.Response {
	// Parse either a params array of numbers or named numbers params.
	var a []float64
	if err := json.Unmarshal(params, &a); err == nil {
		if len(a) != 2 {
			return jrpc.NewInvalidParamsErrorResponse(
				"Invalid number of array params")
		}
		return jrpc.NewResponse(a[0] - a[1])
	}
	var p struct {
		Subtrahend *float64
		Minuend    *float64
	}
	if err := json.Unmarshal(params, &p); err != nil ||
		p.Subtrahend == nil || p.Minuend == nil {
		return jrpc.NewInvalidParamsErrorResponse("Required fields " +
			`"subtrahend" and "minuend" must be valid numbers.`)
	}
	return jrpc.NewResponse(*p.Minuend - *p.Subtrahend)
}
func sum(params json.RawMessage) *jrpc.Response {
	var p []float64
	if err := json.Unmarshal(params, &p); err != nil {
		return jrpc.NewInvalidParamsErrorResponse(nil)
	}
	sum := float64(0)
	for _, x := range p {
		sum += x
	}
	return jrpc.NewResponse(sum)
}
func notifyHello(_ json.RawMessage) *jrpc.Response {
	return jrpc.NewResponse("")
}
func getData(_ json.RawMessage) *jrpc.Response {
	return jrpc.NewResponse([]interface{}{"hello", 5})
}

// This example makes all of the calls from the examples in the JSON-RPC 2.0
// specification and prints them in a similar format.
func main() {
	// Register RPC methods.
	jrpc.RegisterMethod("subtract", subtract)
	jrpc.RegisterMethod("sum", sum)
	jrpc.RegisterMethod("notify_hello", notifyHello)
	jrpc.RegisterMethod("get_data", getData)

	// Start the server.
	go func() {
		http.ListenAndServe(":18888", jrpc.HTTPRequestHandler)
	}()

	// Make requests.
	fmt.Println("Syntax:")
	fmt.Println("--> data sent to Server")
	fmt.Println("<-- data sent to Client")
	fmt.Println("")

	fmt.Println("rpc call with positional parameters:")
	postNewRequest("subtract", 1, []int{42, 23})
	postNewRequest("subtract", 2, []int{23, 42})

	fmt.Println("rpc call with named parameters:")
	postNewRequest("subtract", 3, map[string]int{"subtrahend": 23, "minuend": 42})
	postNewRequest("subtract", 4, map[string]int{"minuend": 42, "subtrahend": 23})

	fmt.Println("a Notification:")
	postNewRequest("update", nil, []int{1, 2, 3, 4, 5})
	postNewRequest("foobar", nil, nil)
	fmt.Println()

	fmt.Println("rpc call of non-existent method:")
	postNewRequest("foobar", "1", nil)

	fmt.Println("rpc call with invalid JSON:")
	postBytes(`{"jsonrpc":"2.0","method":"foobar,"params":"bar","baz]`)

	fmt.Println("rpc call with invalid Request object:")
	postBytes(`{"jsonrpc":"2.0","method":1,"params":"bar"}`)

	fmt.Println("rpc call Batch, invalid JSON:")
	postBytes(
		`[
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method"
]`)

	fmt.Println("rpc call with an empty Array:")
	postBytes(`[]`)

	fmt.Println("rpc call with an invalid Batch (but not empty):")
	postBytes(`[1]`)

	fmt.Println("rpc call with invalid Batch:")
	postBytes(`[1,2,3]`)

	fmt.Println("rpc call Batch:")
	postBytes(`[
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]},
  {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":"2"},
  {"foo":"boo"},
  {"jsonrpc":"2.0","method":"foo.get","params":{"name":"myself"},"id":"5"},
  {"jsonrpc":"2.0","method":"get_data","id":"9"}
]`)
	fmt.Println("rpc call Batch (all notifications):")
	postRequest(jrpc.BatchRequest{
		jrpc.NewNotification("notify_sum", []int{1, 2, 4}),
		jrpc.NewNotification("notify_hello", []int{7}),
	})
	fmt.Println("<-- //Nothing is returned for all notification batches")

}
Output:

Syntax:
--> data sent to Server
<-- data sent to Client

rpc call with positional parameters:
--> {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1}
<-- {"jsonrpc":"2.0","result":19,"id":1}

--> {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":2}
<-- {"jsonrpc":"2.0","result":-19,"id":2}

rpc call with named parameters:
--> {"jsonrpc":"2.0","method":"subtract","params":{"minuend":42,"subtrahend":23},"id":3}
<-- {"jsonrpc":"2.0","result":19,"id":3}

--> {"jsonrpc":"2.0","method":"subtract","params":{"minuend":42,"subtrahend":23},"id":4}
<-- {"jsonrpc":"2.0","result":19,"id":4}

a Notification:
--> {"jsonrpc":"2.0","method":"update","params":[1,2,3,4,5]}
--> {"jsonrpc":"2.0","method":"foobar"}

rpc call of non-existent method:
--> {"jsonrpc":"2.0","method":"foobar","id":"1"}
<-- {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":"1"}

rpc call with invalid JSON:
--> {"jsonrpc":"2.0","method":"foobar,"params":"bar","baz]
<-- {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}

rpc call with invalid Request object:
--> {"jsonrpc":"2.0","method":1,"params":"bar"}
<-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}

rpc call Batch, invalid JSON:
--> [
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method"
]
<-- {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}

rpc call with an empty Array:
--> []
<-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}

rpc call with an invalid Batch (but not empty):
--> [1]
<-- [
  {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
]

rpc call with invalid Batch:
--> [1,2,3]
<-- [
  {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
  {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
  {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
]

rpc call Batch:
--> [
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]},
  {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":"2"},
  {"foo":"boo"},
  {"jsonrpc":"2.0","method":"foo.get","params":{"name":"myself"},"id":"5"},
  {"jsonrpc":"2.0","method":"get_data","id":"9"}
]
<-- [
  {"jsonrpc":"2.0","result":7,"id":"1"},
  {"jsonrpc":"2.0","result":19,"id":"2"},
  {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
  {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":"5"},
  {"jsonrpc":"2.0","result":["hello",5],"id":"9"}
]

rpc call Batch (all notifications):
--> [
  {"jsonrpc":"2.0","method":"notify_sum","params":[1,2,4]},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]}
]
<-- //Nothing is returned for all notification batches

Index

Examples

Constants

View Source
const (
	LowestReservedErrorCode  = -32768
	ParseErrorCode           = -32700
	InvalidRequestCode       = -32600
	MethodNotFoundCode       = -32601
	InvalidParamsCode        = -32602
	InternalErrorCode        = -32603
	HighestReservedErrorCode = -32000

	ParseErrorMessage     = "Parse error"
	InvalidRequestMessage = "Invalid Request"
	MethodNotFoundMessage = "Method not found"
	InvalidParamsMessage  = "Invalid params"
	InternalErrorMessage  = "Internal error"
)

Official JSON-RPC 2.0 Spec Error Codes and Messages

Variables

View Source
var (
	// ParseError is returned to the client if a JSON is not well formed.
	ParseError = *newError(ParseErrorCode, ParseErrorMessage, nil)
	// InvalidRequest is returned to the client if a request does not
	// conform to JSON-RPC 2.0 spec
	InvalidRequest = *newError(InvalidRequestCode, InvalidRequestMessage, nil)
	// MethodNotFound is returned to the client if a method is called that
	// has not been registered with RegisterMethod()
	MethodNotFound = *newError(MethodNotFoundCode, MethodNotFoundMessage, nil)
	// InvalidParams is returned to the client if a method is called with
	// an invalid "params" object. A method's function is responsible for
	// detecting and returning this error.
	InvalidParams = *newError(InvalidParamsCode, InvalidParamsMessage, nil)
	// InternalError is returned to the client if a method function returns
	// an invalid response object.
	InternalError = *newError(InternalErrorCode, InternalErrorMessage, nil)
)

Official Errors

HTTPRequestHandler is a convenience adapter to allow the use of HTTPRequestHandlerFunc as an HTTP handler.

Functions

func HTTPRequestHandlerFunc

func HTTPRequestHandlerFunc(w http.ResponseWriter, req *http.Request)

HTTPRequestHandlerFunc implements an http.HandlerFunc to handle incoming HTTP JSON-RPC 2.0 requests. It handles both single and batch Requests, detects and handles ParseError, InvalidRequest, and MethodNotFound errors, calls the method if the request is valid and the method name has been registered with RegisterMethod, and finally returns the results of any non-notification Requests.

func RegisterMethod

func RegisterMethod(name string, function MethodFunc) error

RegisterMethod registers a new RPC method named name that calls function. RegisterMethod is not thread safe. All RPC methods should be registered from a single thread and prior to serving requests with HTTPRequestHandler. This will return an error if either function is nil or name has already been registered.

See MethodFunc for more information on writing conforming methods.

Types

type BatchRequest

type BatchRequest []*Request

BatchRequest is a type that implements String() for a slice of Requests.

func (BatchRequest) String

func (br BatchRequest) String() string

String returns a string of the JSON array with "--> " prefixed to represent a BatchRequest object.

type BatchResponse

type BatchResponse []*Response

BatchResponse is a type that implements String() for a slice of Responses.

func (BatchResponse) String

func (br BatchResponse) String() string

String returns a string of the JSON array with "<-- " prefixed to represent a BatchResponse object.

type Error

type Error struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

Error represents the "error" field in a JSON-RPC 2.0 Response object.

type MethodFunc

type MethodFunc func(params json.RawMessage) *Response

MethodFunc is the type of function that can be registered as an RPC method. When called it will be passed a params object of type json.RawMessage. It should return a pointer to a valid Response object with either Response.Result or Response.Error populated.

If Response.Error is populated, Response.Result will be removed from the Response before sending it to the client. Any Response.Error.Code returned must either use the InvalidParamsCode, OR use an Error.Code outside of the reserved range (LowestReservedErrorCode - HighestReservedErrorCode) AND have a non-empty Response.Error.Message, which SHOULD be limited to a concise single sentence. Any additional Error.Data may also be provided.

If a MethodFunc panics when it is called, or if it returns an invalid response, an InternalError will be sent to the client if it was not a Notification Request.

Example (Panic)

Any panic will return InternalError to the user if the call was a request and not a Notification.

var alwaysPanic jsonrpc2.MethodFunc = func(params json.RawMessage) *jsonrpc2.Response {
	panic("don't worry, jsonrpc2 will recover you and return an internal error")
}
jsonrpc2.RegisterMethod("panic at the disco!", alwaysPanic)
Output:

func (MethodFunc) Call

func (method MethodFunc) Call(params json.RawMessage) (res *Response)

Call is used by HTTPRequestHandlerFunc to safely call a method, recover from panics, and sanitize its returned Response. If method panics or returns an invalid response, an InternalError response is returned. Error responses are stripped of any Result.

See MethodFunc for more information on writing conforming methods.

type Request

type Request struct {
	JSONRPC string      `json:"jsonrpc"`
	Method  string      `json:"method"`
	Params  interface{} `json:"params,omitempty"`
	ID      interface{} `json:"id,omitempty"`
}

Request represents a JSON-RPC 2.0 Request or Notification object.

Example

Use the http and json packages to send a Request object.

reqBytes, _ := json.Marshal(jsonrpc2.NewRequest("subtract", 0, []int{5, 1}))
httpResp, _ := http.Post("http://localhost:8888", "application/json", bytes.NewReader(reqBytes))
respBytes, _ := ioutil.ReadAll(httpResp.Body)
response := &jsonrpc2.Response{}
json.Unmarshal(respBytes, response)
Output:

func NewNotification

func NewNotification(method string, params interface{}) *Request

NewNotification is a convenience function that returns a new Request with no ID and the "jsonrpc" field already populated with the required value, "2.0". When a request does not have an id, it is a JSON-RPC 2.0 Notification object.

func NewRequest

func NewRequest(method string, id, params interface{}) *Request

NewRequest is a convenience function that returns a new Request with the "jsonrpc" field already populated with the required value, "2.0". If no id is provided, it will be considered a Notification object and not receive a response. Use NewNotification if you want a simpler function call to form a JSON-RPC 2.0 Notification object.

func (*Request) IsValid

func (r *Request) IsValid() bool

IsValid returns true when r has a valid JSONRPC value of "2.0", a non-empty Method, and, if not nil, valid ID and Params types.

func (*Request) String

func (r *Request) String() string

String returns a JSON string with "--> " prefixed to represent a Request object.

type Response

type Response struct {
	JSONRPC string      `json:"jsonrpc"`
	Result  interface{} `json:"result,omitempty"`
	Error   *Error      `json:"error,omitempty"`
	ID      interface{} `json:"id"`
}

Response represents a JSON-RPC 2.0 Response object.

func NewErrorResponse

func NewErrorResponse(code int, message string, data interface{}) *Response

NewErrorResponse is a convenience function that returns a new error Response with JSONRPC field already populated with the required value, "2.0".

func NewInvalidParamsErrorResponse

func NewInvalidParamsErrorResponse(data interface{}) *Response

NewInvalidParamsErrorResponse is a convenience function that returns a properly formed InvalidParams error Response with the given data.

func NewResponse

func NewResponse(result interface{}) *Response

NewResponse is a convenience function that returns a new success Response with JSONRPC already populated with the required value, "2.0".

func (*Response) IsValid

func (r *Response) IsValid() bool

IsValid returns true when r has a valid JSONRPC value of "2.0" and one of Result or Error is not nil.

func (*Response) String

func (r *Response) String() string

String returns a string of the JSON with "<-- " prefixed to represent a Response object.

Jump to

Keyboard shortcuts

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