mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-09 22:47:24 +00:00
* rpc: Add caching support (#9650)
* Set cache control in the HTTP-RPC response header
* Add a simply cache policy to the RPC routes
* add a condition to check the RPC request has default height settings
* fix cherry pick error
* update pending log
* use options struct intead of single parameter
* refacor FuncOptions to functional options
* add functional options in WebSocket RPC function
* revert doc
* replace deprecated function call
* revise functional options
* remove unuse comment
* fix revised error
* adjust cache-control settings
* Update rpc/jsonrpc/server/http_json_handler.go
Co-authored-by: Thane Thomson <connect@thanethomson.com>
* linter: Fix false positive
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* rpc: Separate cacheable and non-cacheable HTTP response writers
Allows us to roll this change out in a non-API-breaking way, since this
is an additive change.
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* rpc: Ensure consistent caching strategy
Ensure a consistent caching strategy across both JSONRPC- and URI-based
requests.
This requires a bit of a refactor of the previous caching logic, which
is complicated a little by the complex reflection-based approach taken
in the Tendermint RPC.
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* rpc: Add more tests for caching
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* Update CHANGELOG_PENDING
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* light: Sync routes config with RPC core
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* rpc: Update OpenAPI docs
Signed-off-by: Thane Thomson <connect@thanethomson.com>
Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: jayt106 <jaytseng106@gmail.com>
Co-authored-by: jay tseng <jay.tseng@crypto.com>
Co-authored-by: JayT106 <JayT106@users.noreply.github.com>
(cherry picked from commit 816c6bac00)
# Conflicts:
# CHANGELOG_PENDING.md
# light/proxy/routes.go
# rpc/core/routes.go
# rpc/openapi/openapi.yaml
# test/fuzz/tests/rpc_jsonrpc_server_test.go
* Fix conflict in CHANGELOG_PENDING
Signed-off-by: Thane Thomson <connect@thanethomson.com>
* Resolve remaining conflicts
Signed-off-by: Thane Thomson <connect@thanethomson.com>
Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: Thane Thomson <connect@thanethomson.com>
219 lines
5.4 KiB
Go
219 lines
5.4 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
tmjson "github.com/tendermint/tendermint/libs/json"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
types "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
|
)
|
|
|
|
// HTTP + URI handler
|
|
|
|
var reInt = regexp.MustCompile(`^-?[0-9]+$`)
|
|
|
|
// convert from a function name to the http handler
|
|
func makeHTTPHandler(rpcFunc *RPCFunc, logger log.Logger) func(http.ResponseWriter, *http.Request) {
|
|
// Always return -1 as there's no ID here.
|
|
dummyID := types.JSONRPCIntID(-1) // URIClientRequestID
|
|
|
|
// Exception for websocket endpoints
|
|
if rpcFunc.ws {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
res := types.RPCMethodNotFoundError(dummyID)
|
|
if wErr := WriteRPCResponseHTTPError(w, http.StatusNotFound, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// All other endpoints
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
logger.Debug("HTTP HANDLER", "req", r)
|
|
|
|
ctx := &types.Context{HTTPReq: r}
|
|
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
|
|
fnArgs, err := httpParamsToArgs(rpcFunc, r)
|
|
if err != nil {
|
|
res := types.RPCInvalidParamsError(dummyID,
|
|
fmt.Errorf("error converting http params to arguments: %w", err),
|
|
)
|
|
if wErr := WriteRPCResponseHTTPError(w, http.StatusInternalServerError, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
return
|
|
}
|
|
args = append(args, fnArgs...)
|
|
|
|
returns := rpcFunc.f.Call(args)
|
|
|
|
logger.Debug("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns)
|
|
result, err := unreflectResult(returns)
|
|
if err != nil {
|
|
if err := WriteRPCResponseHTTPError(w, http.StatusInternalServerError,
|
|
types.RPCInternalError(dummyID, err)); err != nil {
|
|
logger.Error("failed to write response", "res", result, "err", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
resp := types.NewRPCSuccessResponse(dummyID, result)
|
|
if rpcFunc.cacheableWithArgs(args) {
|
|
err = WriteCacheableRPCResponseHTTP(w, resp)
|
|
} else {
|
|
err = WriteRPCResponseHTTP(w, resp)
|
|
}
|
|
if err != nil {
|
|
logger.Error("failed to write response", "res", result, "err", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Covert an http query to a list of properly typed values.
|
|
// To be properly decoded the arg must be a concrete type from tendermint (if its an interface).
|
|
func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) {
|
|
// skip types.Context
|
|
const argsOffset = 1
|
|
|
|
values := make([]reflect.Value, len(rpcFunc.argNames))
|
|
|
|
for i, name := range rpcFunc.argNames {
|
|
argType := rpcFunc.args[i+argsOffset]
|
|
|
|
values[i] = reflect.Zero(argType) // set default for that type
|
|
|
|
arg := getParam(r, name)
|
|
// log.Notice("param to arg", "argType", argType, "name", name, "arg", arg)
|
|
|
|
if arg == "" {
|
|
continue
|
|
}
|
|
|
|
v, ok, err := nonJSONStringToArg(argType, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ok {
|
|
values[i] = v
|
|
continue
|
|
}
|
|
|
|
values[i], err = jsonStringToArg(argType, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return values, nil
|
|
}
|
|
|
|
func jsonStringToArg(rt reflect.Type, arg string) (reflect.Value, error) {
|
|
rv := reflect.New(rt)
|
|
err := tmjson.Unmarshal([]byte(arg), rv.Interface())
|
|
if err != nil {
|
|
return rv, err
|
|
}
|
|
rv = rv.Elem()
|
|
return rv, nil
|
|
}
|
|
|
|
func nonJSONStringToArg(rt reflect.Type, arg string) (reflect.Value, bool, error) {
|
|
if rt.Kind() == reflect.Ptr {
|
|
rv1, ok, err := nonJSONStringToArg(rt.Elem(), arg)
|
|
switch {
|
|
case err != nil:
|
|
return reflect.Value{}, false, err
|
|
case ok:
|
|
rv := reflect.New(rt.Elem())
|
|
rv.Elem().Set(rv1)
|
|
return rv, true, nil
|
|
default:
|
|
return reflect.Value{}, false, nil
|
|
}
|
|
} else {
|
|
return _nonJSONStringToArg(rt, arg)
|
|
}
|
|
}
|
|
|
|
// NOTE: rt.Kind() isn't a pointer.
|
|
func _nonJSONStringToArg(rt reflect.Type, arg string) (reflect.Value, bool, error) {
|
|
isIntString := reInt.Match([]byte(arg))
|
|
isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`)
|
|
isHexString := strings.HasPrefix(strings.ToLower(arg), "0x")
|
|
|
|
var expectingString, expectingByteSlice, expectingInt bool
|
|
switch rt.Kind() {
|
|
case reflect.Int,
|
|
reflect.Uint,
|
|
reflect.Int8,
|
|
reflect.Uint8,
|
|
reflect.Int16,
|
|
reflect.Uint16,
|
|
reflect.Int32,
|
|
reflect.Uint32,
|
|
reflect.Int64,
|
|
reflect.Uint64:
|
|
expectingInt = true
|
|
case reflect.String:
|
|
expectingString = true
|
|
case reflect.Slice:
|
|
expectingByteSlice = rt.Elem().Kind() == reflect.Uint8
|
|
}
|
|
|
|
if isIntString && expectingInt {
|
|
qarg := `"` + arg + `"`
|
|
rv, err := jsonStringToArg(rt, qarg)
|
|
if err != nil {
|
|
return rv, false, err
|
|
}
|
|
|
|
return rv, true, nil
|
|
}
|
|
|
|
if isHexString {
|
|
if !expectingString && !expectingByteSlice {
|
|
err := fmt.Errorf("got a hex string arg, but expected '%s'",
|
|
rt.Kind().String())
|
|
return reflect.ValueOf(nil), false, err
|
|
}
|
|
|
|
var value []byte
|
|
value, err := hex.DecodeString(arg[2:])
|
|
if err != nil {
|
|
return reflect.ValueOf(nil), false, err
|
|
}
|
|
if rt.Kind() == reflect.String {
|
|
return reflect.ValueOf(string(value)), true, nil
|
|
}
|
|
return reflect.ValueOf(value), true, nil
|
|
}
|
|
|
|
if isQuotedString && expectingByteSlice {
|
|
v := reflect.New(reflect.TypeOf(""))
|
|
err := tmjson.Unmarshal([]byte(arg), v.Interface())
|
|
if err != nil {
|
|
return reflect.ValueOf(nil), false, err
|
|
}
|
|
v = v.Elem()
|
|
return reflect.ValueOf([]byte(v.String())), true, nil
|
|
}
|
|
|
|
return reflect.ValueOf(nil), false, nil
|
|
}
|
|
|
|
func getParam(r *http.Request, param string) string {
|
|
s := r.URL.Query().Get(param)
|
|
if s == "" {
|
|
s = r.FormValue(param)
|
|
}
|
|
return s
|
|
}
|