-
Notifications
You must be signed in to change notification settings - Fork 296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
rpcserver: add batched request support (json 2.0) #841
Conversation
@davecgh PR should be good now. |
Does this apply only to HTTP requests or also to websockets? |
@chappjc applies to websockets as well. |
rpcserver.go
Outdated
jsonErr = rpcInvalidError("limited user not " + | ||
"authorized for this method") | ||
// determine if the request is single or batched | ||
reqType := reflect.TypeOf(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of reflect, use switch/case:
switch request.(type) {
case map[string]interface{}:
...
}
@dajohi updated the PR per your review. |
rpcserver.go
Outdated
// determine if the request is single or batched | ||
switch request.(type) { | ||
case map[string]interface{}: | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you drop the scope and save on indentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing, on it.
@dajohi PR updated. |
rpcserver.go
Outdated
switch request.(type) { | ||
case map[string]interface{}: | ||
request := request.(map[string]interface{}) | ||
req, err := dcrjson.NewRequest(request["id"].(interface{}), request["method"].(string), request["params"].([]interface{})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please wrap at 80 columns
rpcserver.go
Outdated
batchedReply.WriteByte('[') | ||
for idx, entry := range reqs { | ||
entry := entry.(map[string]interface{}) | ||
req, err := dcrjson.NewRequest(entry["id"].(interface{}), entry["method"].(string), entry["params"].([]interface{})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please wrap at 80 columns
rpcwebsocket.go
Outdated
switch request.(type) { | ||
case map[string]interface{}: | ||
data := request.(map[string]interface{}) | ||
req, err := dcrjson.NewRequest(data["id"].(interface{}), data["method"].(string), data["params"].([]interface{})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please wrap at 80 columns
rpcwebsocket.go
Outdated
var replies [][]byte | ||
for _, entry := range batch { | ||
entry := entry.(map[string]interface{}) | ||
req, err := dcrjson.NewRequest(entry["id"].(interface{}), entry["method"].(string), entry["params"].([]interface{})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please wrap at 80 columns
@dajohi PR updated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be the last of my nitpicking
Looks good! 👍
rpcserver.go
Outdated
jsonErr = rpcInvalidError("limited user not " + | ||
"authorized for this method") | ||
// determine if the request is single or batched | ||
switch request.(type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a default case that does something like
default:
rpcslog.Debugf("unhandled request due to unknown type: %T", request)
rpcwebsocket.go
Outdated
if cmd.err != nil { | ||
if !c.authenticated { | ||
// Determine if the request is single request or a batched request | ||
switch request.(type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a default case that does something like
default:
rpcslog.Debugf("unhandled request due to unknown type: %T", request)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, on it.
@dajohi PR updated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this can't go in as is. There are a lot of issues such as failing to check error returns, the ability to crash it due to naked type casts, inefficiencies due to improper use of interfaces instead of unmarshalling into the appropriate dcrjson.Result
type, and not following the spec for malformed requests.
rpcwebsocket.go
Outdated
// QueueNotification which implements a queue via notificationQueueHandler to | ||
// ensure sending notifications from other subsystems can't block. Ultimately, | ||
// all messages are sent via the outHandler. | ||
// wsClient provides an abstraction for handling a websocket client. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is not wrapped at 80 cols properly.
It should be:
// wsClient provides an abstraction for handling a websocket client. The
// overall data flow is split into 3 main goroutines. A websocket manager is
// used to allow things such as broadcasting requested notifications to all
// connected websocket clients. Inbound messages are read via the inHandler
// goroutine and generally dispatched to their own handler. There are two
// outbound message types - one for responding to client requests and another
// for async notifications. Responses to client requests use SendMessage which
// employs a buffered channel thereby limiting the number of outstanding
// requests that can be made. Notifications are sent via QueueNotification
// which implements a queue via notificationQueueHandler to ensure sending
// notifications from other subsystems can't block. Ultimately, all messages
// are sent via the outHandler.
Good editors are capable of doing this wrapping for you if they're configured properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will address the issues pointed out, thanks.
rpcserver.go
Outdated
var result interface{} | ||
var request dcrjson.Request | ||
var msg []byte | ||
var request interface{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify a bit on what I mean about interfaces, notice the code here is doing the unmarshal into an interface. Instead, it would be more efficient and safer to look at the byte stream in order to determine whether it should unmarshal into a Request
or a []Request
in order to be properly typed.
For example:
// At package scope...
var batchedRequestPrefix = []byte("[{")
...
if bytes.HasPrefix(body, batchedRequestPrefix) {
/* attempt unmarshal to []Request */
} else {
/* attempt unmarshal to Request */
}
EDIT: For a bit more of an update on this, upon looking at the specification again, I see that each individual request within the batch requires an individual response unless the entire JSON fails to parse. That means the initial unmarshal will indeed need to go into a []interface
, however, every element within that array should be unmarshalled into a Request
instead of manually trying to do naked type casts.
Here are the three relevant examples from the spec:
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 invalid Batch, but valid JSON:
--> [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 with mixed valid and invalid requests, but all valid JSON:
--> [
{"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"}
]
rpcserver.go
Outdated
batchedReply.WriteByte('[') | ||
for idx, entry := range reqs { | ||
entry := entry.(map[string]interface{}) | ||
req, err := dcrjson.NewRequest(entry["id"].(interface{}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of what I meant by naked type casts that can crash.
Notice what would happen if you fed this code requests like:
[{"jsonrpc":1.0", "bogus":"foo"}]
or [{"jsonrpc":1.0", "id":"1", params:"foo"}]
ba4f6ae
to
79ec1b9
Compare
@davecgh I've updated the http post implementation, the websocket impl. will be up in a couple of days. Would like a review for the http post impl. in the mean time when you're free, thanks. |
updated, should be good now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is producing some incorrect results in some negative paths. See the following code:
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"github.com/decred/dcrd/dcrutil"
)
func main() {
const (
url = "https://127.0.0.1:19109"
rpcUser = "yourrpcuser"
rpcPass = "yourrpcpass"
)
// populate request set
reqs := []string{
`{}`,
`[]`,
`[1]`,
`[1,2,3]`,
`{"foo": "boo"}`, // should be an invalid request
`{"jsonrpc": "1.0", "foo": "boo", "id": "1"}`,
`{"jsonrpc": "1.0", "method": "getblockcount", "params": [], "id": "1"}`,
`{"jsonrpc": "1.0", "method": "getblockcount", "params": "a", "id": "1"}`, // should be invalid since params is neither an array nor a json object.
`[
{"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": "1"},
{"jsonrpc": "2.0", "method": "decodescript", "params": ["ac"]},
{"jsonrpc": "2.0", "method": "getbestblockhash", "params": [], "id": "2"},
{"foo": "boo"},
{"jsonrpc": "2.0", "method": "getblockcount", "id": "9"}
]`, // should produce invalid request for the `{"foo": "boo"}`.
}
// Connect to local dcrd RPC server using websockets.
dcrdHomeDir := dcrutil.AppDataDir("dcrd", false)
certs, err := ioutil.ReadFile(filepath.Join(dcrdHomeDir, "rpc.cert"))
if err != nil {
fmt.Println(err)
return
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(certs)
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
},
},
}
for _, jsonReq := range reqs {
bodyReader := bytes.NewReader([]byte(jsonReq))
httpReq, err := http.NewRequest("POST", url, bodyReader)
if err != nil {
fmt.Println(err)
return
}
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json")
httpReq.SetBasicAuth(rpcUser, rpcPass)
resp, err := client.Do(httpReq)
if err != nil {
fmt.Println("request:", jsonReq, "response:", err)
return
}
respBytes, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("request:", jsonReq, "response:", string(respBytes))
}
}
Output:
request: {} response:
request: [] response: {"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: empty batch"},"id":null}
request: [1] response: [{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null}]
request: [1,2,3] response: [{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null},{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null},{"jsonrpc":"1.0","result":null,"error":{"code":-32600,"message":"Invalid request: json: cannot unmarshal number into Go value of type dcrjson.Request"},"id":null}]
request: {"foo": "boo"} response:
request: {"jsonrpc": "1.0", "foo": "boo", "id": "1"} response: {"jsonrpc":"1.0","result":null,"error":{"code":-32601,"message":"Method not found"},"id":"1"}
request: {"jsonrpc": "1.0", "method": "getblockcount", "params": [], "id": "1"} response: {"jsonrpc":"1.0","result":227411,"error":null,"id":"1"}
request: {"jsonrpc": "1.0", "method": "getblockcount", "params": "a", "id": "1"} response: {"jsonrpc":"1.0","result":227411,"error":null,"id":"1"}
request: [
{"jsonrpc": "2.0", "method": "getblockcount", "params": [], "id": "1"},
{"jsonrpc": "2.0", "method": "decodescript", "params": ["ac"]},
{"jsonrpc": "2.0", "method": "getbestblockhash", "params": [], "id": "2"},
{"foo": "boo"},
{"jsonrpc": "2.0", "method": "getblockcount", "id": "9"}
] response: [{"jsonrpc":"2.0","result":227411,"error":null,"id":"1"},{"jsonrpc":"2.0","result":"0000000003563140b1bd52599ac1abca98900bce048e9c3ba9ca72eba948f7d6","error":null,"id":"2"},{"jsonrpc":"2.0","result":227411,"error":null,"id":"9"}]
dcrjson/jsonrpc.go
Outdated
// Response is the general form of a JSON-RPC response. The type of the Result | ||
// field varies from one command to the next, so it is implemented as an | ||
// interface. The ID field has to be a pointer for Go to put a null in it when | ||
// Response is the general form of a JSON-RPC 1.0 response. The type of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the addition of 1.0? This applies to both versions.
The final |
3a2040f
to
5b4cafd
Compare
dcrjson/jsonrpc.go
Outdated
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd | ||
// function with that command to generate the marshalled JSON-RPC request. | ||
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) { | ||
func (request *Request) UnmarshalJSON(b []byte) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing function comment.
rpcserver.go
Outdated
} | ||
|
||
// processRequest determines incoming request type (single or batched), | ||
// parses it and returns a response. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/response/marshalled response/
rpcwebsocket.go
Outdated
if err != nil { | ||
rpcsLog.Errorf("Failed to marshal parse failure "+ | ||
"reply: %v", err) | ||
cmd := parseCmd(&req) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this should retain the previous functionality of checking for req.ID == nil
first to avoid the extra overhead of parsing notifications. I agree that this is correct behavior due to the addition of the check for nil in the parseCmd
function, but there is non reason to even create the struct and churn GC for notifications.
rpcwebsocket.go
Outdated
Code: dcrjson.ErrRPCInvalidParams.Code, | ||
Message: "limited user not authorized for this method", | ||
rpcsLog.Debugf("Received command <%s> from %s", cmd.method, c.addr) | ||
// Check auth. The client is immediately disconnected if the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a blank line before this to separate the logical blocks.
a819423
to
949b22b
Compare
Is there an example of using this on an http post? |
It's the same as a regular request except you create an array of of them: e.g.
|
No description provided.