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 ¶
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 ¶
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
var HTTPRequestHandler = http.HandlerFunc(HTTPRequestHandlerFunc)
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 ¶
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 ¶
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.
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 ¶
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".