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 http.HandlerFuncs that call the MethodFuncs in a given MethodMap. The http.HandlerFuncs will recover from any MethodFunc panics and will always respond with a valid JSON RPC Response, unless of course the request 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 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))
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/v6"
)
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)
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 ¶
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
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 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.
type ErrorCode ¶
type ErrorCode int
ErrorCode represent the int JSON RPC 2.0 error code.
func (ErrorCode) IsReserved ¶
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 called 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.
A valid Response must have either a Result or Error populated.
If Error is populated, the Result will be discarded and the Error will be validated. Valid errors will always retain their Data.
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, so the MethodFunc does not need to ensure that the Message is populated in this case. Otherwise the Message must be populated and the ErrorCode must not be within the reserved ErrorCode range.
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")
}
func (MethodFunc) Call ¶
func (method MethodFunc) Call(params json.RawMessage) (res Response)
Call is used to safely call a method from within an http.HandlerFunc. Call wraps the actual invocation of the method so that it can recover from panics and validate and sanitize the returned Response. If the method panics or returns an invalid Response, an InternalError response is returned.
Valid error Responses are stripped of any Result left over by the method, and any user provided Data is Marshaled and replaced with the resulting json.RawMessage.
For valid Responses, the user provided Result is Marshaled and replaced with the resulting json.RawMessage.
If you are getting InternalErrors from your method, set DebugMethodFunc to true for additional debug output about the cause of the internal error.
See MethodFunc for more information on writing conforming methods.
type MethodMap ¶
type MethodMap map[string]MethodFunc
MethodMap associates method names with MethodFuncs and is passed to HTTPRequestHandler() to generate a corresponding http.HandlerFunc.
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)
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 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.
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.
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".