Add support for JSON-RPC batched calls (#341)

This PR introduces proper support for JSON-RPC batched requests (http://www.jsonrpc.org/specification#batch)
This commit is contained in:
Ivan Daniluk 2017-09-18 16:24:35 +02:00 committed by Ivan Tomilov
parent 5f19c9cd0a
commit ca4bc5152f
5 changed files with 112 additions and 54 deletions

View File

@ -1,5 +1,5 @@
# rpc [![GoDoc](https://godoc.org/github.com/status-im/status-go/geth/rpc?status.png)](https://godoc.org/github.com/status-im/status-go/geth/rpc)
rpc - JSON-RPC client with custom routing.
Package rpc - JSON-RPC client with custom routing.
Download:
```shell
@ -7,7 +7,7 @@ go get github.com/status-im/status-go/geth/rpc
```
* * *
rpc - JSON-RPC client with custom routing.
Package rpc - JSON-RPC client with custom routing.
Package rpc implements status-go JSON-RPC client and handles
requests to different endpoints: upstream or local node.
@ -27,4 +27,4 @@ Note, upon creation of a new client, it ok to be offline - client will keep tryi
* * *
Automatically generated by [autoreadme](https://github.com/jimmyfrasche/autoreadme) on 2017.09.15
Automatically generated by [autoreadme](https://github.com/jimmyfrasche/autoreadme) on 2017.09.18

View File

@ -3,7 +3,6 @@ package rpc
import (
"context"
"encoding/json"
"errors"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/geth/log"
@ -26,7 +25,7 @@ var defaultMsgID = json.RawMessage(`0`)
// returns string in JSON format with response (successul or error).
func (c *Client) CallRaw(body string) string {
ctx := context.Background()
return c.callRawContext(ctx, body)
return c.callRawContext(ctx, json.RawMessage(body))
}
// jsonrpcMessage represents JSON-RPC request, notification, successful response or
@ -57,9 +56,51 @@ type jsonError struct {
// This is waste of CPU and memory and should be avoided if possible,
// either by changing exported API (provide only Call, not CallRaw) or
// refactoring go-ethereum's client to allow using raw JSON directly.
func (c *Client) callRawContext(ctx context.Context, body string) string {
func (c *Client) callRawContext(ctx context.Context, body json.RawMessage) string {
if isBatch(body) {
return c.callBatchMethods(ctx, body)
}
return c.callSingleMethod(ctx, body)
}
// callBatchMethods handles batched JSON-RPC requests, calling each of
// individual requests one by one and constructing proper batched response.
//
// See http://www.jsonrpc.org/specification#batch for details.
//
// We can't use gethtrpc.BatchCall here, because each call should go through
// our routing logic and router to corresponding destination.
func (c *Client) callBatchMethods(ctx context.Context, msgs json.RawMessage) string {
var requests []json.RawMessage
err := json.Unmarshal(msgs, &requests)
if err != nil {
return newErrorResponse(errInvalidMessageCode, err, defaultMsgID)
}
// run all methods sequentially, this seems to be main
// objective to use batched requests.
// See: https://github.com/ethereum/wiki/wiki/JavaScript-API#batch-requests
responses := make([]json.RawMessage, len(requests))
for i := range requests {
resp := c.callSingleMethod(ctx, requests[i])
responses[i] = json.RawMessage(resp)
}
data, err := json.Marshal(responses)
if err != nil {
log.Error("Failed to marshal batch responses:", err)
return newErrorResponse(errInvalidMessageCode, err, defaultMsgID)
}
return string(data)
}
// callSingleMethod executes single JSON-RPC message and constructs proper response.
func (c *Client) callSingleMethod(ctx context.Context, msg json.RawMessage) string {
// unmarshal JSON body into json-rpc request
method, params, id, err := methodAndParamsFromBody(body)
method, params, id, err := methodAndParamsFromBody(msg)
if err != nil {
return newErrorResponse(errInvalidMessageCode, err, id)
}
@ -87,7 +128,7 @@ func (c *Client) callRawContext(ctx context.Context, body string) string {
// JSON-RPC body into values ready to use with ethereum-go's
// RPC client Call() function. A lot of empty interface usage is
// due to the underlying code design :/
func methodAndParamsFromBody(body string) (string, []interface{}, json.RawMessage, error) {
func methodAndParamsFromBody(body json.RawMessage) (string, []interface{}, json.RawMessage, error) {
msg, err := unmarshalMessage(body)
if err != nil {
return "", nil, nil, err
@ -105,43 +146,12 @@ func methodAndParamsFromBody(body string) (string, []interface{}, json.RawMessag
}
// unmarshalMessage tries to unmarshal JSON-RPC message.
// somehow JSON-RPC input from web3.js can be in two forms:
//
// object: {"jsonrpc":"2.0", …}
// array: [{"jsonrpc":"2.0", …}]
//
// unmarhsalMessage tries first option and in case of error,
// tries to unmarshal it as an array.
//
// TODO(divan): fix the source of this error and cleanup.
func unmarshalMessage(body string) (*jsonrpcMessage, error) {
func unmarshalMessage(body json.RawMessage) (*jsonrpcMessage, error) {
var msg jsonrpcMessage
err := json.Unmarshal([]byte(body), &msg)
// check for array case
if e, ok := err.(*json.UnmarshalTypeError); ok {
if e.Value == "array" {
return unmarshalMessageArray(body)
}
}
err := json.Unmarshal(body, &msg)
return &msg, err
}
func unmarshalMessageArray(body string) (*jsonrpcMessage, error) {
var msgs []*jsonrpcMessage
err := json.Unmarshal([]byte(body), &msgs)
if err != nil {
return nil, err
}
// return first element
if len(msgs) == 0 {
return nil, errors.New("empty array")
} else if len(msgs) > 1 {
log.Warn("JSON-RPC payload has more then 1 objects", "len", len(msgs), "body", body)
}
return msgs[0], nil
}
func newSuccessResponse(result json.RawMessage, id json.RawMessage) string {
if id == nil {
id = defaultMsgID
@ -173,3 +183,16 @@ func newErrorResponse(code int, err error, id json.RawMessage) string {
data, _ := json.Marshal(errMsg)
return string(data)
}
// isBatch returns true when the first non-whitespace characters is '['
// code from go-ethereum's rpc client (rpc/client.go)
func isBatch(msg json.RawMessage) bool {
for _, c := range msg {
// skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt)
if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d {
continue
}
return c == '['
}
return false
}

View File

@ -36,7 +36,7 @@ func TestNewErrorResponse(t *testing.T) {
}
func TestUnmarshalMessage(t *testing.T) {
body := `{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}`
body := json.RawMessage(`{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}}`)
got, err := unmarshalMessage(body)
require.NoError(t, err)
@ -51,7 +51,7 @@ func TestUnmarshalMessage(t *testing.T) {
func TestMethodAndParamsFromBody(t *testing.T) {
cases := []struct {
name string
body string
body json.RawMessage
params []interface{}
method string
id json.RawMessage
@ -59,7 +59,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
}{
{
"params_array",
`{"jsonrpc": "2.0", "id": 42, "method": "subtract", "params": [{"subtrahend": 23, "minuend": 42}]}`,
json.RawMessage(`{"jsonrpc": "2.0", "id": 42, "method": "subtract", "params": [{"subtrahend": 23, "minuend": 42}]}`),
[]interface{}{
map[string]interface{}{
"subtrahend": float64(23),
@ -72,7 +72,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
},
{
"params_empty_array",
`{"jsonrpc": "2.0", "method": "test", "params": []}`,
json.RawMessage(`{"jsonrpc": "2.0", "method": "test", "params": []}`),
[]interface{}{},
"test",
nil,
@ -80,7 +80,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
},
{
"params_none",
`{"jsonrpc": "2.0", "method": "test"}`,
json.RawMessage(`{"jsonrpc": "2.0", "method": "test"}`),
[]interface{}{},
"test",
nil,
@ -88,7 +88,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
},
{
"getFilterMessage",
`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`,
json.RawMessage(`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`),
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
"shh_getFilterMessages",
json.RawMessage(`44`),
@ -96,15 +96,15 @@ func TestMethodAndParamsFromBody(t *testing.T) {
},
{
"getFilterMessage_array",
`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`,
[]interface{}{string("3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e")},
"shh_getFilterMessages",
json.RawMessage(`44`),
false,
json.RawMessage(`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`),
[]interface{}{},
"",
nil,
true,
},
{
"empty_array",
`[]`,
json.RawMessage(`[]`),
[]interface{}{},
"",
nil,
@ -117,6 +117,7 @@ func TestMethodAndParamsFromBody(t *testing.T) {
method, params, id, err := methodAndParamsFromBody(test.body)
if test.shouldFail {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, test.method, method)
@ -125,3 +126,21 @@ func TestMethodAndParamsFromBody(t *testing.T) {
})
}
}
func TestIsBatch(t *testing.T) {
cases := []struct {
name string
body json.RawMessage
expected bool
}{
{"single", json.RawMessage(`{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}`), false},
{"array", json.RawMessage(`[{"jsonrpc":"2.0","id":44,"method":"shh_getFilterMessages","params":["3de6a8867aeb75be74d68478b853b4b0e063704d30f8231c45d0fcbd97af207e"]}]`), true},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
got := isBatch(test.body)
require.Equal(t, test.expected, got)
})
}
}

View File

@ -209,6 +209,22 @@ func (s *RPCTestSuite) TestCallRPC() {
progress <- struct{}{}
},
},
{
`[{"jsonrpc":"2.0","method":"net_version","params":[],"id":67}]`,
func(resultJSON string) {
expected := `[{"jsonrpc":"2.0","id":67,"result":"4"}]`
s.Equal(expected, resultJSON)
progress <- struct{}{}
},
},
{
`[{"jsonrpc":"2.0","method":"net_version","params":[],"id":67},{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":68}]`,
func(resultJSON string) {
expected := `[{"jsonrpc":"2.0","id":67,"result":"4"},{"jsonrpc":"2.0","id":68,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}]`
s.Equal(expected, resultJSON)
progress <- struct{}{}
},
},
}
cnt := len(rpcCalls) - 1 // send transaction blocks up until complete/discarded/times out

View File

@ -1,5 +1,5 @@
/*
rpc - JSON-RPC client with custom routing.
Package rpc - JSON-RPC client with custom routing.
Package rpc implements status-go JSON-RPC client and handles
requests to different endpoints: upstream or local node.