mirror of
https://github.com/fluencelabs/tendermint
synced 2025-06-13 13:21:20 +00:00
* Fixed accepting integer IDs in requests for Tendermint RPC server (#2366) * added a wrapper interface `jsonrpcid` that represents both string and int IDs in JSON-RPC requests/responses + custom JSON unmarshallers * changed client-side code in RPC that uses it * added extra tests for integer IDs * updated CHANGELOG_PENDING, as suggested by PR instructions * addressed PR comments * added table driven tests for request type marshalling/unmarshalling * expanded handler test to check IDs * changed pending changelog note * changed json rpc request/response unmarshalling to use empty interfaces and type switches on ID * some cleanup
This commit is contained in:
committed by
Ethan Buchman
parent
b487feba42
commit
b12488b5f1
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@ -13,17 +14,75 @@ import (
|
||||
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
||||
)
|
||||
|
||||
// a wrapper to emulate a sum type: jsonrpcid = string | int
|
||||
// TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412
|
||||
type jsonrpcid interface {
|
||||
isJSONRPCID()
|
||||
}
|
||||
|
||||
// JSONRPCStringID a wrapper for JSON-RPC string IDs
|
||||
type JSONRPCStringID string
|
||||
|
||||
func (JSONRPCStringID) isJSONRPCID() {}
|
||||
|
||||
// JSONRPCIntID a wrapper for JSON-RPC integer IDs
|
||||
type JSONRPCIntID int
|
||||
|
||||
func (JSONRPCIntID) isJSONRPCID() {}
|
||||
|
||||
func idFromInterface(idInterface interface{}) (jsonrpcid, error) {
|
||||
switch id := idInterface.(type) {
|
||||
case string:
|
||||
return JSONRPCStringID(id), nil
|
||||
case float64:
|
||||
// json.Unmarshal uses float64 for all numbers
|
||||
// (https://golang.org/pkg/encoding/json/#Unmarshal),
|
||||
// but the JSONRPC2.0 spec says the id SHOULD NOT contain
|
||||
// decimals - so we truncate the decimals here.
|
||||
return JSONRPCIntID(int(id)), nil
|
||||
default:
|
||||
typ := reflect.TypeOf(id)
|
||||
return nil, fmt.Errorf("JSON-RPC ID (%v) is of unknown type (%v)", id, typ)
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
// REQUEST
|
||||
|
||||
type RPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
ID jsonrpcid `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{}
|
||||
}
|
||||
|
||||
func NewRPCRequest(id string, method string, params json.RawMessage) RPCRequest {
|
||||
// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int
|
||||
func (request *RPCRequest) UnmarshalJSON(data []byte) error {
|
||||
unsafeReq := &struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{}
|
||||
}{}
|
||||
err := json.Unmarshal(data, &unsafeReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.JSONRPC = unsafeReq.JSONRPC
|
||||
request.Method = unsafeReq.Method
|
||||
request.Params = unsafeReq.Params
|
||||
if unsafeReq.ID == nil {
|
||||
return nil
|
||||
}
|
||||
id, err := idFromInterface(unsafeReq.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRPCRequest(id jsonrpcid, method string, params json.RawMessage) RPCRequest {
|
||||
return RPCRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
@ -36,7 +95,7 @@ func (req RPCRequest) String() string {
|
||||
return fmt.Sprintf("[%s %s]", req.ID, req.Method)
|
||||
}
|
||||
|
||||
func MapToRequest(cdc *amino.Codec, id string, method string, params map[string]interface{}) (RPCRequest, error) {
|
||||
func MapToRequest(cdc *amino.Codec, id jsonrpcid, method string, params map[string]interface{}) (RPCRequest, error) {
|
||||
var params_ = make(map[string]json.RawMessage, len(params))
|
||||
for name, value := range params {
|
||||
valueJSON, err := cdc.MarshalJSON(value)
|
||||
@ -53,7 +112,7 @@ func MapToRequest(cdc *amino.Codec, id string, method string, params map[string]
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func ArrayToRequest(cdc *amino.Codec, id string, method string, params []interface{}) (RPCRequest, error) {
|
||||
func ArrayToRequest(cdc *amino.Codec, id jsonrpcid, method string, params []interface{}) (RPCRequest, error) {
|
||||
var params_ = make([]json.RawMessage, len(params))
|
||||
for i, value := range params {
|
||||
valueJSON, err := cdc.MarshalJSON(value)
|
||||
@ -89,12 +148,38 @@ func (err RPCError) Error() string {
|
||||
|
||||
type RPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
ID jsonrpcid `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *RPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func NewRPCSuccessResponse(cdc *amino.Codec, id string, res interface{}) RPCResponse {
|
||||
// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int
|
||||
func (response *RPCResponse) UnmarshalJSON(data []byte) error {
|
||||
unsafeResp := &struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *RPCError `json:"error,omitempty"`
|
||||
}{}
|
||||
err := json.Unmarshal(data, &unsafeResp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response.JSONRPC = unsafeResp.JSONRPC
|
||||
response.Error = unsafeResp.Error
|
||||
response.Result = unsafeResp.Result
|
||||
if unsafeResp.ID == nil {
|
||||
return nil
|
||||
}
|
||||
id, err := idFromInterface(unsafeResp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRPCSuccessResponse(cdc *amino.Codec, id jsonrpcid, res interface{}) RPCResponse {
|
||||
var rawMsg json.RawMessage
|
||||
|
||||
if res != nil {
|
||||
@ -109,7 +194,7 @@ func NewRPCSuccessResponse(cdc *amino.Codec, id string, res interface{}) RPCResp
|
||||
return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg}
|
||||
}
|
||||
|
||||
func NewRPCErrorResponse(id string, code int, msg string, data string) RPCResponse {
|
||||
func NewRPCErrorResponse(id jsonrpcid, code int, msg string, data string) RPCResponse {
|
||||
return RPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
@ -124,27 +209,27 @@ func (resp RPCResponse) String() string {
|
||||
return fmt.Sprintf("[%s %s]", resp.ID, resp.Error)
|
||||
}
|
||||
|
||||
func RPCParseError(id string, err error) RPCResponse {
|
||||
func RPCParseError(id jsonrpcid, err error) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error())
|
||||
}
|
||||
|
||||
func RPCInvalidRequestError(id string, err error) RPCResponse {
|
||||
func RPCInvalidRequestError(id jsonrpcid, err error) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error())
|
||||
}
|
||||
|
||||
func RPCMethodNotFoundError(id string) RPCResponse {
|
||||
func RPCMethodNotFoundError(id jsonrpcid) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32601, "Method not found", "")
|
||||
}
|
||||
|
||||
func RPCInvalidParamsError(id string, err error) RPCResponse {
|
||||
func RPCInvalidParamsError(id jsonrpcid, err error) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error())
|
||||
}
|
||||
|
||||
func RPCInternalError(id string, err error) RPCResponse {
|
||||
func RPCInternalError(id jsonrpcid, err error) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32603, "Internal error", err.Error())
|
||||
}
|
||||
|
||||
func RPCServerError(id string, err error) RPCResponse {
|
||||
func RPCServerError(id jsonrpcid, err error) RPCResponse {
|
||||
return NewRPCErrorResponse(id, -32000, "Server error", err.Error())
|
||||
}
|
||||
|
||||
|
@ -15,24 +15,57 @@ type SampleResult struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type responseTest struct {
|
||||
id jsonrpcid
|
||||
expected string
|
||||
}
|
||||
|
||||
var responseTests = []responseTest{
|
||||
{JSONRPCStringID("1"), `"1"`},
|
||||
{JSONRPCStringID("alphabet"), `"alphabet"`},
|
||||
{JSONRPCStringID(""), `""`},
|
||||
{JSONRPCStringID("àáâ"), `"àáâ"`},
|
||||
{JSONRPCIntID(-1), "-1"},
|
||||
{JSONRPCIntID(0), "0"},
|
||||
{JSONRPCIntID(1), "1"},
|
||||
{JSONRPCIntID(100), "100"},
|
||||
}
|
||||
|
||||
func TestResponses(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cdc := amino.NewCodec()
|
||||
for _, tt := range responseTests {
|
||||
jsonid := tt.id
|
||||
a := NewRPCSuccessResponse(cdc, jsonid, &SampleResult{"hello"})
|
||||
b, _ := json.Marshal(a)
|
||||
s := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected)
|
||||
assert.Equal(string(s), string(b))
|
||||
|
||||
a := NewRPCSuccessResponse(cdc, "1", &SampleResult{"hello"})
|
||||
b, _ := json.Marshal(a)
|
||||
s := `{"jsonrpc":"2.0","id":"1","result":{"Value":"hello"}}`
|
||||
assert.Equal(string(s), string(b))
|
||||
d := RPCParseError(jsonid, errors.New("Hello world"))
|
||||
e, _ := json.Marshal(d)
|
||||
f := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`, tt.expected)
|
||||
assert.Equal(string(f), string(e))
|
||||
|
||||
d := RPCParseError("1", errors.New("Hello world"))
|
||||
e, _ := json.Marshal(d)
|
||||
f := `{"jsonrpc":"2.0","id":"1","error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`
|
||||
assert.Equal(string(f), string(e))
|
||||
g := RPCMethodNotFoundError(jsonid)
|
||||
h, _ := json.Marshal(g)
|
||||
i := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32601,"message":"Method not found"}}`, tt.expected)
|
||||
assert.Equal(string(h), string(i))
|
||||
}
|
||||
}
|
||||
|
||||
g := RPCMethodNotFoundError("2")
|
||||
h, _ := json.Marshal(g)
|
||||
i := `{"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"Method not found"}}`
|
||||
assert.Equal(string(h), string(i))
|
||||
func TestUnmarshallResponses(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cdc := amino.NewCodec()
|
||||
for _, tt := range responseTests {
|
||||
response := &RPCResponse{}
|
||||
err := json.Unmarshal([]byte(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected)), response)
|
||||
assert.Nil(err)
|
||||
a := NewRPCSuccessResponse(cdc, tt.id, &SampleResult{"hello"})
|
||||
assert.Equal(*response, a)
|
||||
}
|
||||
response := &RPCResponse{}
|
||||
err := json.Unmarshal([]byte(`{"jsonrpc":"2.0","id":true,"result":{"Value":"hello"}}`), response)
|
||||
assert.NotNil(err)
|
||||
}
|
||||
|
||||
func TestRPCError(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user