jsonrpc2

package module
v8.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 30, 2018 License: MIT Imports: 7 Imported by: 0

README

jsonrpc2/v7

GoDoc Go Report Card Coverage Status Build Status

Package jsonrpc2 is a conforming implementation of the JSON-RPC 2.0 protocol designed to provide a minimalist API, avoid unnecessary unmarshaling and memory allocation, and work with any http server framework that uses http.Handler.

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

Getting started

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

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, and relate a name to the method using a MethodMap.

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

Read the documentation for MethodFunc and MethodMap for more information.

Finally generate an http.HandlerFunc for your MethodMap and start your server.

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

Documentation

Overview

Package jsonrpc2 is a conforming implementation of the JSON-RPC 2.0 protocol designed to provide a minimalist API, avoid unnecessary unmarshaling and memory allocation, and work with any http server framework that uses http.Handler. It strives to conform very strictly to the official specification: https://www.jsonrpc.org.

This package provides types for Requests and Responses, and a function to return a http.HandlerFunc that calls the MethodFuncs in a given MethodMap. The http.HandlerFunc will recover from any MethodFunc panics and will always respond with a valid JSON RPC Response, unless of course the Request did not have an ID, and thus was a Notification.

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{Result: MyCustomResultType{}}
json.Unmarshal(respBytes, &response)

Server

Servers must implement their RPC method functions to match the MethodFunc type, and relate a name to the method using a MethodMap.

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

Read the documentation for MethodFunc and MethodMap for more information.

Finally generate an http.HandlerFunc for your MethodMap and start your server.

http.ListenAndServe(":8080", jsonrpc2.HTTPRequestHandler(methods))
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.

package main

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

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

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() {
	// Start the server.
	go func() {
		// Register RPC methods.
		methods := jrpc.MethodMap{
			"subtract":     subtract,
			"sum":          sum,
			"notify_hello": notifyHello,
			"get_data":     getData,
		}
		handler := jrpc.HTTPRequestHandler(methods)
		jrpc.DebugMethodFunc = false
		http.ListenAndServe(":18888", handler)
	}()

	// 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.NewRequest("notify_sum", nil, []int{1, 2, 4}),
		jrpc.NewRequest("notify_hello", nil, []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  ErrorCode = -32768
	ParseErrorCode           ErrorCode = -32700
	InvalidRequestCode       ErrorCode = -32600
	MethodNotFoundCode       ErrorCode = -32601
	InvalidParamsCode        ErrorCode = -32602
	InternalErrorCode        ErrorCode = -32603
	HighestReservedErrorCode ErrorCode = -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

View Source
const Version = "2.0"

Version is the valid version string for the "jsonrpc" field required in all JSON RPC 2.0 objects.

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

View Source
var DebugMethodFunc = false

DebugMethodFunc controls whether additional debug information will be printed to stdout in the event of an internal error when a MethodFunc is called. This can be helpful when users are troubleshooting their MethodFuncs.

Functions

func HTTPRequestHandler

func HTTPRequestHandler(methods MethodMap) http.HandlerFunc

HTTPRequestHandler returns an http.HandlerFunc for the provided methods MethodMap. HTTPRequestHandler panics if methods.IsValid() returns an error.

The returned http.HandlerFunc handles single and batch HTTP JSON-RPC 2.0 requests. It also deals with ParseError, InvalidRequest, and MethodNotFound errors. For valid requests, it calls the corresponding MethodFunc and returns any results or errors for any non-notification Requests.

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    ErrorCode   `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

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

func NewError

func NewError(code ErrorCode, message string, data interface{}) Error

NewError returns an Error with the given code, message, and data.

type ErrorCode

type ErrorCode int

ErrorCode represent the int JSON RPC 2.0 error code.

func (ErrorCode) IsReserved

func (c ErrorCode) IsReserved() bool

IsReserved returns true if c is within the reserved error code range.

type MethodFunc

type MethodFunc func(params json.RawMessage) Response

MethodFunc is the function signature used for RPC methods. The raw JSON of the params of a valid Request is passed to the MethodFunc for further application specific unmarshaling and validation. When a MethodFunc is invoked by the handler, the params json.RawMessage, if not nil, is guaranteed to be valid JSON representing a structured JSON type.

A valid MethodFunc must return a valid Response object. If MethodFunc panics, or if the returned Response is not valid for whatever reason, then an InternalError with no Data will be returned by the http.HandlerFunc instead.

A valid Response must have either a Result or Error populated.

An Error is considered populated if the Error.Message is not empty. If Error is populated, any Result will be ignored and the Error will be validated.

A valid Error must be either InvalidParams or must use an ErrorCode outside of the reserved range. If the ErrorCode is InvalidParamsCode, then the correct InvalidParamsMessage will be set. In this case the MethodFunc only needs to ensure that the Message is not empty. MethodFuncs are encouraged to use NewInvalidParamsErrorResponse() for these errors.

If you are getting InternalErrors from your method, set DebugMethodFunc to true for additional debug output about the cause of the internal error.

Example (Panic)

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

var _ jsonrpc2.MethodFunc = func(params json.RawMessage) jsonrpc2.Response {
	panic("don't worry, jsonrpc2 will recover you and return an internal error")
}
Output:

type MethodMap

type MethodMap map[string]MethodFunc

MethodMap associates method names with MethodFuncs and is passed to HTTPRequestHandler() to generate a corresponding http.HandlerFunc.

func (MethodMap) IsValid

func (methods MethodMap) IsValid() error

IsValid returns nil if methods is not nil, not empty, and contains no entries with either an empty name or a nil MethodFunc.

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. ID must be a numeric or string type. Params must be a structured type: slice, array, map or struct.

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 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 nil 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 if r has a valid JSONRPC value of "2.0" and a non-empty Method. Params and ID are not validated

func (Request) MarshalJSON

func (r Request) MarshalJSON() ([]byte, error)

MarshalJSON outputs a valid JSON RPC Request object. The Response.JSONRPC field is always output as Version ("2.0").

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   `json:"error,omitempty"`
	ID      interface{} `json:"id"`
}

Response represents a JSON-RPC 2.0 Response object.

The json:",omitempty" are simply here for clarity. Although Error is a concrete type and the json package will not ever detect it as being empty, the json.Marhsaler interface is implemented to use the Error.Message length to determine whether the Error should be considered empty.

func NewErrorResponse

func NewErrorResponse(code ErrorCode, 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) IsError

func (r Response) IsError() bool

IsError returns true if r.Error.Message is not empty.

func (Response) IsValid

func (r Response) IsValid() bool

IsValid returns true if either Response.Result is not nil or Response.Error.Message is not empty.

func (Response) MarshalJSON

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

MarshalJSON outputs a valid JSON RPC Response object. It returns an error if Response.Result and Response.Error are both empty. The Error is considered empty if the Error.Message is empty. If the Error is not empty, any Result will be omitted. If the Error is empty, then the Error is omitted. The Response.JSONRPC field is always output as Version ("2.0").

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