jsonrpc2

package module
v2.0.0+incompatible Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2018 License: MIT Imports: 4 Imported by: 1

README

jsonrpc2 - v2.0.0

GoDoc Go Report Card

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 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.

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.

type MethodFunc func(params interface{}) Response

Methods must be registered with a name using RegisterMethod().

jsonrpc2.RegisterMethod("subtract", mySubtractMethodFunc)

Read the documentation for RegisterMethod and MethodFunc for more information.

For convenience, methods can use RemarshalJSON() for converting the abstract params argument into a custom concrete type.

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 v2.0.0
// 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"
)

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 interface{}) jrpc.Response {
	// Parse either a params array of numbers or named numbers params.
	switch params.(type) {
	case []interface{}:
		var p []float64
		if err := jrpc.RemarshalJSON(&p, params); err != nil ||
			len(p) != 2 {
			return jrpc.NewInvalidParamsErrorResponse(
				"Invalid number of array params")
		}
		return jrpc.NewResponse(p[0] - p[1])
	case interface{}:
		var p struct {
			Subtrahend *float64
			Minuend    *float64
		}
		if err := jrpc.RemarshalJSON(&p, params); 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)
	}
	// The jsonrpc2 package guarantees this will never happen, so it should
	// be regarded as an InternalError.
	panic("unexpected params type")
}
func sum(params interface{}) jrpc.Response {
	var p []float64
	if err := jrpc.RemarshalJSON(&p, params); err != nil {
		return jrpc.NewInvalidParamsErrorResponse(nil)
	}
	sum := float64(0)
	for _, x := range p {
		sum += x
	}
	return jrpc.NewResponse(sum)
}
func notifyHello(params interface{}) jrpc.Response {
	return jrpc.NewResponse("")
}
func getData(params interface{}) 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 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.

func RemarshalJSON

func RemarshalJSON(dst, src interface{}) error

RemarshalJSON unmarshals src and remarshals it into dst as an easy way for a MethodFunc to marshal its provided params object into a more specific custom type. For this function to have any effect dst must be a pointer to a type that supports json.Unmarshal.

Example (NamedParams)

If a method function expects named params object with two numbers named "A" and "B" it could use the following anonymous struct to remarshal its given params argument. Note the use of pointers to detect the presence of individual parameters.

package main

import (
	"github.com/AdamSLevy/jsonrpc2"
)

func main() {
	var subtract jsonrpc2.MethodFunc = func(params interface{}) jsonrpc2.Response {
		var p struct {
			A *float64
			B *float64
		}
		if err := jsonrpc2.RemarshalJSON(&p, params); err != nil ||
			p.A == nil || p.B == nil {
			return jsonrpc2.NewInvalidParamsErrorResponse(nil)
		}
		return jsonrpc2.NewResponse(*p.A - *p.B)
	}
	jsonrpc2.RegisterMethod("subtract", subtract)
}
Output:

Example (ParamsArrayMultipleTypes)

If a method expects a params array of multiple types, there is no type it can be directly remarshaled into other than []interface{}, from there each individual param will need to be checked with a safe type assertion.

package main

import (
	"github.com/AdamSLevy/jsonrpc2"
)

func main() {
	jsonrpc2.RegisterMethod("repeat-string",
		func(params interface{}) jsonrpc2.Response {
			// Verify this is a params array of length 2.
			var p []interface{}
			if err := jsonrpc2.RemarshalJSON(&p, params); err != nil || len(p) != 2 {
				return jsonrpc2.NewInvalidParamsErrorResponse(nil)
			}
			// Verify that the arguments are a string and a number.
			var s string
			var ok bool
			if s, ok = p[0].(string); !ok {
				return jsonrpc2.NewInvalidParamsErrorResponse(nil)
			}
			var f float64
			if f, ok = p[1].(float64); !ok {
				return jsonrpc2.NewInvalidParamsErrorResponse(nil)
			}
			// Repeat s n times.
			var n = int(f)
			var result string
			for i := 0; i < n; i++ {
				result += s
			}
			return jsonrpc2.NewResponse(result)
		})
}
Output:

Example (ParamsArraySingleType)

If a method function expects a params array of a single type, it can use a slice of that type with RemarshalJSON.

package main

import (
	"github.com/AdamSLevy/jsonrpc2"
)

func main() {
	jsonrpc2.RegisterMethod("subtract",
		func(params interface{}) jsonrpc2.Response {
			var p []float64
			if err := jsonrpc2.RemarshalJSON(&p, params); err != nil ||
				len(p) != 2 {
				return jsonrpc2.NewInvalidParamsErrorResponse(
					"Must be an array of two valid numbers")
			}
			return jsonrpc2.NewResponse(p[0] - p[1])
		})
}
Output:

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 interface{}) 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 either type []interface{} or map[string]interface{}. It should return 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.

package main

import (
	"github.com/AdamSLevy/jsonrpc2"
)

func main() {
	var alwaysPanic jsonrpc2.MethodFunc = func(params interface{}) 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 interface{}) (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.

package main

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

	"github.com/AdamSLevy/jsonrpc2"
)

func main() {
	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