Skip to content

Commit 31fcf6a

Browse files
authored
refactor(rpc): handlers.go - makeJSONRPCHandler() control flow refactor (#3716)
I am proposing a simpler way to write the makeJSONRPCHandler function. There was a recent bug due to this function that turned lists of requests with a single item to a object that is a response--but not a list of responses. With this refactor, this function would be more resilient to bugs because of its simplified control flow: it first tries to unmarshal as a slice of requests, and returns an array of responses if successful. It then tries to unmarshal it into a single request, and returns a single response if successful. This refactor also abstracts the logic that processes the requests out to `processRequests(r, request, funcMap, logger)` to see if they actually need to be sent (eg. notifications do not need to be sent). All of that logic is now in the processRequest function, and improves on the previous implementation because now makeJSONRPCHandle is much easier to read and understand. If both unmarshals fail, the function returns an error "error unmarshalling request," same as before. Please let me know what you think of the refactor. Thanks.
1 parent febedbf commit 31fcf6a

File tree

1 file changed

+66
-56
lines changed

1 file changed

+66
-56
lines changed

tm2/pkg/bft/rpc/lib/server/handlers.go

+66-56
Original file line numberDiff line numberDiff line change
@@ -140,74 +140,84 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H
140140
return
141141
}
142142

143-
// first try to unmarshal the incoming request as an array of RPC requests
144-
var (
145-
requests types.RPCRequests
146-
responses types.RPCResponses
147-
)
148-
149-
// isRPCRequestArray is used to determine if the incoming payload is an array of requests.
150-
// This flag helps decide whether to return an array of responses (for batch requests) or a single response.
151-
isRPCRequestArray := true
143+
// --- Branch 1: Attempt to Unmarshal as a Batch (Slice) of Requests ---
144+
var requests types.RPCRequests
145+
if err := json.Unmarshal(b, &requests); err == nil {
146+
var responses types.RPCResponses
147+
for _, req := range requests {
148+
if resp := processRequest(r, req, funcMap, logger); resp != nil {
149+
responses = append(responses, *resp)
150+
}
151+
}
152152

153-
if err := json.Unmarshal(b, &requests); err != nil {
154-
// next, try to unmarshal as a single request
155-
var request types.RPCRequest
156-
if err := json.Unmarshal(b, &request); err != nil {
157-
WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request")))
153+
if len(responses) > 0 {
154+
WriteRPCResponseArrayHTTP(w, responses)
158155
return
159156
}
160-
requests = []types.RPCRequest{request}
161-
isRPCRequestArray = false
162157
}
163158

164-
for _, request := range requests {
165-
request := request
166-
// A Notification is a Request object without an "id" member.
167-
// The Server MUST NOT reply to a Notification, including those that are within a batch request.
168-
if request.ID == types.JSONRPCStringID("") {
169-
logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)")
170-
continue
171-
}
172-
if len(r.URL.Path) > 1 {
173-
responses = append(responses, types.RPCInvalidRequestError(request.ID, errors.New("path %s is invalid", r.URL.Path)))
174-
continue
175-
}
176-
rpcFunc, ok := funcMap[request.Method]
177-
if !ok || rpcFunc.ws {
178-
responses = append(responses, types.RPCMethodNotFoundError(request.ID))
179-
continue
180-
}
181-
ctx := &types.Context{JSONReq: &request, HTTPReq: r}
182-
args := []reflect.Value{reflect.ValueOf(ctx)}
183-
if len(request.Params) > 0 {
184-
fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params)
185-
if err != nil {
186-
responses = append(responses, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "error converting json params to arguments")))
187-
continue
188-
}
189-
args = append(args, fnArgs...)
190-
}
191-
returns := rpcFunc.f.Call(args)
192-
logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns)
193-
result, err := unreflectResult(returns)
194-
if err != nil {
195-
responses = append(responses, types.RPCInternalError(request.ID, err))
196-
continue
159+
// --- Branch 2: Attempt to Unmarshal as a Single Request ---
160+
var request types.RPCRequest
161+
if err := json.Unmarshal(b, &request); err == nil {
162+
if resp := processRequest(r, request, funcMap, logger); resp != nil {
163+
WriteRPCResponseHTTP(w, *resp)
164+
return
197165
}
198-
responses = append(responses, types.NewRPCSuccessResponse(request.ID, result))
199-
}
200-
if len(responses) == 0 {
166+
} else {
167+
WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request")))
201168
return
202169
}
170+
}
171+
}
203172

204-
if isRPCRequestArray {
205-
WriteRPCResponseArrayHTTP(w, responses)
206-
return
173+
// processRequest checks and processes a single JSON-RPC request.
174+
// If the request should produce a response, it returns a pointer to that response.
175+
// Otherwise (e.g. if the request is a notification or fails validation), it returns nil.
176+
func processRequest(r *http.Request, req types.RPCRequest, funcMap map[string]*RPCFunc, logger *slog.Logger) *types.RPCResponse {
177+
// Skip notifications (an empty ID indicates no response should be sent)
178+
if req.ID == types.JSONRPCStringID("") {
179+
logger.Debug("Skipping notification (empty ID)")
180+
return nil
181+
}
182+
183+
// Check that the URL path is valid (assume only "/" is acceptable)
184+
if len(r.URL.Path) > 1 {
185+
resp := types.RPCInvalidRequestError(req.ID, fmt.Errorf("invalid path: %s", r.URL.Path))
186+
return &resp
187+
}
188+
189+
// Look up the requested method in the function map.
190+
rpcFunc, ok := funcMap[req.Method]
191+
if !ok || rpcFunc.ws {
192+
resp := types.RPCMethodNotFoundError(req.ID)
193+
return &resp
194+
}
195+
196+
ctx := &types.Context{JSONReq: &req, HTTPReq: r}
197+
args := []reflect.Value{reflect.ValueOf(ctx)}
198+
if len(req.Params) > 0 {
199+
fnArgs, err := jsonParamsToArgs(rpcFunc, req.Params)
200+
if err != nil {
201+
resp := types.RPCInvalidParamsError(req.ID, errors.Wrap(err, "error converting json params to arguments"))
202+
return &resp
207203
}
204+
args = append(args, fnArgs...)
205+
}
208206

209-
WriteRPCResponseHTTP(w, responses[0])
207+
// Call the RPC function using reflection.
208+
returns := rpcFunc.f.Call(args)
209+
logger.Info("HTTPJSONRPC", "method", req.Method, "args", args, "returns", returns)
210+
211+
// Convert the reflection return values into a result value for JSON serialization.
212+
result, err := unreflectResult(returns)
213+
if err != nil {
214+
resp := types.RPCInternalError(req.ID, err)
215+
return &resp
210216
}
217+
218+
// Build and return a successful response.
219+
resp := types.NewRPCSuccessResponse(req.ID, result)
220+
return &resp
211221
}
212222

213223
func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc {

0 commit comments

Comments
 (0)