mirror of
https://github.com/tendermint/tendermint.git
synced 2026-05-23 23:51:35 +00:00
We should not set cache-control headers on RPC responses. HTTP caching interacts poorly with resources that are expected to change frequently, or whose rate of change is unpredictable. More subtly, all calls to the POST endpoint use the same URL, which means a cacheable response from one call may actually "hide" an uncacheable response from a subsequent one. This is less of a problem for the GET endpoints, but that means the behaviour of RPCs varies depending on which HTTP method your client happens to use. Websocket requests were already marked statically uncacheable, adding yet a third combination. To address this: - Stop setting cache-control headers. - Update the tests that were checking for those headers. - Remove the flags to request cache-control. Apart from affecting the HTTP response headers, this change does not modify the behaviour of any of the RPC methods.
245 lines
6.3 KiB
Go
245 lines
6.3 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
tmjson "github.com/tendermint/tendermint/libs/json"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
"github.com/tendermint/tendermint/rpc/coretypes"
|
|
rpctypes "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 := rpctypes.JSONRPCIntID(-1) // URIClientRequestID
|
|
|
|
// Exception for websocket endpoints
|
|
if rpcFunc.ws {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
res := rpctypes.RPCMethodNotFoundError(dummyID)
|
|
if wErr := WriteRPCResponseHTTPError(w, 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", dumpHTTPRequest(r))
|
|
|
|
ctx := rpctypes.WithCallInfo(r.Context(), &rpctypes.CallInfo{HTTPRequest: r})
|
|
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
|
|
fnArgs, err := httpParamsToArgs(rpcFunc, r)
|
|
if err != nil {
|
|
res := rpctypes.RPCInvalidParamsError(dummyID,
|
|
fmt.Errorf("error converting http params to arguments: %w", err),
|
|
)
|
|
if wErr := WriteRPCResponseHTTPError(w, 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)
|
|
switch e := err.(type) {
|
|
// if no error then return a success response
|
|
case nil:
|
|
res := rpctypes.NewRPCSuccessResponse(dummyID, result)
|
|
if wErr := WriteRPCResponseHTTP(w, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
|
|
// if this already of type RPC error then forward that error.
|
|
case *rpctypes.RPCError:
|
|
res := rpctypes.NewRPCErrorResponse(dummyID, e.Code, e.Message, e.Data)
|
|
if wErr := WriteRPCResponseHTTPError(w, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
|
|
default: // we need to unwrap the error and parse it accordingly
|
|
var res rpctypes.RPCResponse
|
|
|
|
switch errors.Unwrap(err) {
|
|
case coretypes.ErrZeroOrNegativeHeight,
|
|
coretypes.ErrZeroOrNegativePerPage,
|
|
coretypes.ErrPageOutOfRange,
|
|
coretypes.ErrInvalidRequest:
|
|
res = rpctypes.RPCInvalidRequestError(dummyID, err)
|
|
default: // ctypes.ErrHeightNotAvailable, ctypes.ErrHeightExceedsChainHead:
|
|
res = rpctypes.RPCInternalError(dummyID, err)
|
|
}
|
|
|
|
if wErr := WriteRPCResponseHTTPError(w, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func dumpHTTPRequest(r *http.Request) string {
|
|
d, e := httputil.DumpRequest(r, true)
|
|
if e != nil {
|
|
return e.Error()
|
|
}
|
|
|
|
return string(d)
|
|
}
|