mirror of
https://github.com/tendermint/tendermint.git
synced 2025-12-23 06:15:19 +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>
This commit is contained in:
@@ -19,5 +19,6 @@
|
||||
### IMPROVEMENTS
|
||||
- [p2p] \#9641 Add new Envelope type and associated methods for sending and receiving Envelopes instead of raw bytes.
|
||||
This also adds new metrics, `tendermint_p2p_message_send_bytes_total` and `tendermint_p2p_message_receive_bytes_total`, that expose how many bytes of each message type have been sent.
|
||||
- [rpc] \#9666 Enable caching of RPC responses (@JayT106)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
@@ -21,20 +21,20 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc {
|
||||
"health": rpcserver.NewRPCFunc(makeHealthFunc(c), ""),
|
||||
"status": rpcserver.NewRPCFunc(makeStatusFunc(c), ""),
|
||||
"net_info": rpcserver.NewRPCFunc(makeNetInfoFunc(c), ""),
|
||||
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight"),
|
||||
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""),
|
||||
"genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), ""),
|
||||
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"),
|
||||
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash"),
|
||||
"block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height"),
|
||||
"commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height"),
|
||||
"tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove"),
|
||||
"blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight", rpcserver.Cacheable()),
|
||||
"genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), "", rpcserver.Cacheable()),
|
||||
"genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), "", rpcserver.Cacheable()),
|
||||
"block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height", rpcserver.Cacheable("height")),
|
||||
"block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash", rpcserver.Cacheable()),
|
||||
"block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height", rpcserver.Cacheable("height")),
|
||||
"commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height", rpcserver.Cacheable("height")),
|
||||
"tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove", rpcserver.Cacheable()),
|
||||
"tx_search": rpcserver.NewRPCFunc(makeTxSearchFunc(c), "query,prove,page,per_page,order_by"),
|
||||
"block_search": rpcserver.NewRPCFunc(makeBlockSearchFunc(c), "query,page,per_page,order_by"),
|
||||
"validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height,page,per_page"),
|
||||
"validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height,page,per_page", rpcserver.Cacheable("height")),
|
||||
"dump_consensus_state": rpcserver.NewRPCFunc(makeDumpConsensusStateFunc(c), ""),
|
||||
"consensus_state": rpcserver.NewRPCFunc(makeConsensusStateFunc(c), ""),
|
||||
"consensus_params": rpcserver.NewRPCFunc(makeConsensusParamsFunc(c), "height"),
|
||||
"consensus_params": rpcserver.NewRPCFunc(makeConsensusParamsFunc(c), "height", rpcserver.Cacheable("height")),
|
||||
"unconfirmed_txs": rpcserver.NewRPCFunc(makeUnconfirmedTxsFunc(c), "limit"),
|
||||
"num_unconfirmed_txs": rpcserver.NewRPCFunc(makeNumUnconfirmedTxsFunc(c), ""),
|
||||
|
||||
@@ -45,7 +45,7 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc {
|
||||
|
||||
// abci API
|
||||
"abci_query": rpcserver.NewRPCFunc(makeABCIQueryFunc(c), "path,data,height,prove"),
|
||||
"abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), ""),
|
||||
"abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), "", rpcserver.Cacheable()),
|
||||
|
||||
// evidence API
|
||||
"broadcast_evidence": rpcserver.NewRPCFunc(makeBroadcastEvidenceFunc(c), "evidence"),
|
||||
|
||||
@@ -17,21 +17,21 @@ var Routes = map[string]*rpc.RPCFunc{
|
||||
"health": rpc.NewRPCFunc(Health, ""),
|
||||
"status": rpc.NewRPCFunc(Status, ""),
|
||||
"net_info": rpc.NewRPCFunc(NetInfo, ""),
|
||||
"blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"),
|
||||
"genesis": rpc.NewRPCFunc(Genesis, ""),
|
||||
"genesis_chunked": rpc.NewRPCFunc(GenesisChunked, "chunk"),
|
||||
"block": rpc.NewRPCFunc(Block, "height"),
|
||||
"block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash"),
|
||||
"block_results": rpc.NewRPCFunc(BlockResults, "height"),
|
||||
"commit": rpc.NewRPCFunc(Commit, "height"),
|
||||
"check_tx": rpc.NewRPCFunc(CheckTx, "tx"),
|
||||
"tx": rpc.NewRPCFunc(Tx, "hash,prove"),
|
||||
"blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight", rpc.Cacheable()),
|
||||
"genesis": rpc.NewRPCFunc(Genesis, "", rpc.Cacheable()),
|
||||
"genesis_chunked": rpc.NewRPCFunc(GenesisChunked, "chunk", rpc.Cacheable()),
|
||||
"block": rpc.NewRPCFunc(Block, "height", rpc.Cacheable("height")),
|
||||
"block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash", rpc.Cacheable()),
|
||||
"block_results": rpc.NewRPCFunc(BlockResults, "height", rpc.Cacheable("height")),
|
||||
"commit": rpc.NewRPCFunc(Commit, "height", rpc.Cacheable("height")),
|
||||
"check_tx": rpc.NewRPCFunc(CheckTx, "tx", rpc.Cacheable()),
|
||||
"tx": rpc.NewRPCFunc(Tx, "hash,prove", rpc.Cacheable()),
|
||||
"tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page,order_by"),
|
||||
"block_search": rpc.NewRPCFunc(BlockSearch, "query,page,per_page,order_by"),
|
||||
"validators": rpc.NewRPCFunc(Validators, "height,page,per_page"),
|
||||
"validators": rpc.NewRPCFunc(Validators, "height,page,per_page", rpc.Cacheable("height")),
|
||||
"dump_consensus_state": rpc.NewRPCFunc(DumpConsensusState, ""),
|
||||
"consensus_state": rpc.NewRPCFunc(ConsensusState, ""),
|
||||
"consensus_params": rpc.NewRPCFunc(ConsensusParams, "height"),
|
||||
"consensus_params": rpc.NewRPCFunc(ConsensusParams, "height", rpc.Cacheable("height")),
|
||||
"unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, "limit"),
|
||||
"num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""),
|
||||
|
||||
@@ -42,7 +42,7 @@ var Routes = map[string]*rpc.RPCFunc{
|
||||
|
||||
// abci API
|
||||
"abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"),
|
||||
"abci_info": rpc.NewRPCFunc(ABCIInfo, ""),
|
||||
"abci_info": rpc.NewRPCFunc(ABCIInfo, "", rpc.Cacheable()),
|
||||
|
||||
// evidence API
|
||||
"broadcast_evidence": rpc.NewRPCFunc(BroadcastEvidence, "evidence"),
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -37,9 +39,7 @@ const (
|
||||
testVal = "acbd"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
)
|
||||
var ctx = context.Background()
|
||||
|
||||
type ResultEcho struct {
|
||||
Value string `json:"value"`
|
||||
@@ -57,6 +57,10 @@ type ResultEchoDataBytes struct {
|
||||
Value tmbytes.HexBytes `json:"value"`
|
||||
}
|
||||
|
||||
type ResultEchoWithDefault struct {
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
// Define some routes
|
||||
var Routes = map[string]*server.RPCFunc{
|
||||
"echo": server.NewRPCFunc(EchoResult, "arg"),
|
||||
@@ -64,6 +68,7 @@ var Routes = map[string]*server.RPCFunc{
|
||||
"echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"),
|
||||
"echo_data_bytes": server.NewRPCFunc(EchoDataBytesResult, "arg"),
|
||||
"echo_int": server.NewRPCFunc(EchoIntResult, "arg"),
|
||||
"echo_default": server.NewRPCFunc(EchoWithDefault, "arg", server.Cacheable("arg")),
|
||||
}
|
||||
|
||||
func EchoResult(ctx *types.Context, v string) (*ResultEcho, error) {
|
||||
@@ -86,6 +91,14 @@ func EchoDataBytesResult(ctx *types.Context, v tmbytes.HexBytes) (*ResultEchoDat
|
||||
return &ResultEchoDataBytes{v}, nil
|
||||
}
|
||||
|
||||
func EchoWithDefault(ctx *types.Context, v *int) (*ResultEchoWithDefault, error) {
|
||||
val := -1
|
||||
if v != nil {
|
||||
val = *v
|
||||
}
|
||||
return &ResultEchoWithDefault{val}, nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
setup()
|
||||
code := m.Run()
|
||||
@@ -199,26 +212,47 @@ func echoDataBytesViaHTTP(cl client.Caller, bytes tmbytes.HexBytes) (tmbytes.Hex
|
||||
return result.Value, nil
|
||||
}
|
||||
|
||||
func echoWithDefaultViaHTTP(cl client.Caller, v *int) (int, error) {
|
||||
params := map[string]interface{}{}
|
||||
if v != nil {
|
||||
params["arg"] = *v
|
||||
}
|
||||
result := new(ResultEchoWithDefault)
|
||||
if _, err := cl.Call(ctx, "echo_default", params, result); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.Value, nil
|
||||
}
|
||||
|
||||
func testWithHTTPClient(t *testing.T, cl client.HTTPClient) {
|
||||
val := testVal
|
||||
got, err := echoViaHTTP(cl, val)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got, val)
|
||||
|
||||
val2 := randBytes(t)
|
||||
got2, err := echoBytesViaHTTP(cl, val2)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got2, val2)
|
||||
|
||||
val3 := tmbytes.HexBytes(randBytes(t))
|
||||
got3, err := echoDataBytesViaHTTP(cl, val3)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got3, val3)
|
||||
|
||||
val4 := tmrand.Intn(10000)
|
||||
got4, err := echoIntViaHTTP(cl, val4)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got4, val4)
|
||||
|
||||
got5, err := echoWithDefaultViaHTTP(cl, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got5, -1)
|
||||
|
||||
val6 := tmrand.Intn(10000)
|
||||
got6, err := echoWithDefaultViaHTTP(cl, &val6)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, got6, val6)
|
||||
}
|
||||
|
||||
func echoViaWS(cl *client.WSClient, val string) (string, error) {
|
||||
@@ -233,7 +267,6 @@ func echoViaWS(cl *client.WSClient, val string) (string, error) {
|
||||
msg := <-cl.ResponsesCh
|
||||
if msg.Error != nil {
|
||||
return "", err
|
||||
|
||||
}
|
||||
result := new(ResultEcho)
|
||||
err = json.Unmarshal(msg.Result, result)
|
||||
@@ -255,7 +288,6 @@ func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) {
|
||||
msg := <-cl.ResponsesCh
|
||||
if msg.Error != nil {
|
||||
return []byte{}, msg.Error
|
||||
|
||||
}
|
||||
result := new(ResultEchoBytes)
|
||||
err = json.Unmarshal(msg.Result, result)
|
||||
@@ -399,6 +431,74 @@ func TestWSClientPingPong(t *testing.T) {
|
||||
time.Sleep(6 * time.Second)
|
||||
}
|
||||
|
||||
func TestJSONRPCCaching(t *testing.T) {
|
||||
httpAddr := strings.Replace(tcpAddr, "tcp://", "http://", 1)
|
||||
cl, err := client.DefaultHTTPClient(httpAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Not supplying the arg should result in not caching
|
||||
params := make(map[string]interface{})
|
||||
req, err := types.MapToRequest(types.JSONRPCIntID(1000), "echo_default", params)
|
||||
require.NoError(t, err)
|
||||
|
||||
res1, err := rawJSONRPCRequest(t, cl, httpAddr, req)
|
||||
defer func() { _ = res1.Body.Close() }()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", res1.Header.Get("Cache-control"))
|
||||
|
||||
// Supplying the arg should result in caching
|
||||
params["arg"] = tmrand.Intn(10000)
|
||||
req, err = types.MapToRequest(types.JSONRPCIntID(1001), "echo_default", params)
|
||||
require.NoError(t, err)
|
||||
|
||||
res2, err := rawJSONRPCRequest(t, cl, httpAddr, req)
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "public, max-age=86400", res2.Header.Get("Cache-control"))
|
||||
}
|
||||
|
||||
func rawJSONRPCRequest(t *testing.T, cl *http.Client, url string, req interface{}) (*http.Response, error) {
|
||||
reqBytes, err := json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
reqBuf := bytes.NewBuffer(reqBytes)
|
||||
httpReq, err := http.NewRequest(http.MethodPost, url, reqBuf)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpReq.Header.Set("Content-type", "application/json")
|
||||
|
||||
return cl.Do(httpReq)
|
||||
}
|
||||
|
||||
func TestURICaching(t *testing.T) {
|
||||
httpAddr := strings.Replace(tcpAddr, "tcp://", "http://", 1)
|
||||
cl, err := client.DefaultHTTPClient(httpAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Not supplying the arg should result in not caching
|
||||
args := url.Values{}
|
||||
res1, err := rawURIRequest(t, cl, httpAddr+"/echo_default", args)
|
||||
defer func() { _ = res1.Body.Close() }()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", res1.Header.Get("Cache-control"))
|
||||
|
||||
// Supplying the arg should result in caching
|
||||
args.Set("arg", fmt.Sprintf("%d", tmrand.Intn(10000)))
|
||||
res2, err := rawURIRequest(t, cl, httpAddr+"/echo_default", args)
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "public, max-age=86400", res2.Header.Get("Cache-control"))
|
||||
}
|
||||
|
||||
func rawURIRequest(t *testing.T, cl *http.Client, url string, args url.Values) (*http.Response, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(args.Encode()))
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func randBytes(t *testing.T) []byte {
|
||||
n := tmrand.Intn(10) + 2
|
||||
buf := make([]byte, n)
|
||||
|
||||
@@ -55,6 +55,11 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
|
||||
requests = []types.RPCRequest{request}
|
||||
}
|
||||
|
||||
// Set the default response cache to true unless
|
||||
// 1. Any RPC request error.
|
||||
// 2. Any RPC request doesn't allow to be cached.
|
||||
// 3. Any RPC request has the height argument and the value is 0 (the default).
|
||||
cache := true
|
||||
for _, request := range requests {
|
||||
request := request
|
||||
|
||||
@@ -72,11 +77,13 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
|
||||
responses,
|
||||
types.RPCInvalidRequestError(request.ID, fmt.Errorf("path %s is invalid", r.URL.Path)),
|
||||
)
|
||||
cache = false
|
||||
continue
|
||||
}
|
||||
rpcFunc, ok := funcMap[request.Method]
|
||||
if !ok || rpcFunc.ws {
|
||||
if !ok || (rpcFunc.ws) {
|
||||
responses = append(responses, types.RPCMethodNotFoundError(request.ID))
|
||||
cache = false
|
||||
continue
|
||||
}
|
||||
ctx := &types.Context{JSONReq: &request, HTTPReq: r}
|
||||
@@ -88,11 +95,16 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
|
||||
responses,
|
||||
types.RPCInvalidParamsError(request.ID, fmt.Errorf("error converting json params to arguments: %w", err)),
|
||||
)
|
||||
cache = false
|
||||
continue
|
||||
}
|
||||
args = append(args, fnArgs...)
|
||||
}
|
||||
|
||||
if cache && !rpcFunc.cacheableWithArgs(args) {
|
||||
cache = false
|
||||
}
|
||||
|
||||
returns := rpcFunc.f.Call(args)
|
||||
result, err := unreflectResult(returns)
|
||||
if err != nil {
|
||||
@@ -103,7 +115,13 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han
|
||||
}
|
||||
|
||||
if len(responses) > 0 {
|
||||
if wErr := WriteRPCResponseHTTP(w, responses...); wErr != nil {
|
||||
var wErr error
|
||||
if cache {
|
||||
wErr = WriteCacheableRPCResponseHTTP(w, responses...)
|
||||
} else {
|
||||
wErr = WriteRPCResponseHTTP(w, responses...)
|
||||
}
|
||||
if wErr != nil {
|
||||
logger.Error("failed to write responses", "res", responses, "err", wErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ import (
|
||||
|
||||
func testMux() *http.ServeMux {
|
||||
funcMap := map[string]*RPCFunc{
|
||||
"c": NewRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"),
|
||||
"c": NewRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"),
|
||||
"block": NewRPCFunc(func(ctx *types.Context, h int) (string, error) { return "block", nil }, "height", Cacheable("height")),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -227,3 +228,52 @@ func TestUnknownRPCPath(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404")
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
func TestRPCResponseCache(t *testing.T) {
|
||||
mux := testMux()
|
||||
body := strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": ["1"]}`)
|
||||
req, _ := http.NewRequest("Get", "http://localhost/", body)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
|
||||
// Always expecting back a JSONRPCResponse
|
||||
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
|
||||
require.Equal(t, "public, max-age=86400", res.Header.Get("Cache-control"))
|
||||
|
||||
_, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.Nil(t, err, "reading from the body should not give back an error")
|
||||
|
||||
// send a request with default height.
|
||||
body = strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": ["0"]}`)
|
||||
req, _ = http.NewRequest("Get", "http://localhost/", body)
|
||||
rec = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
|
||||
// Always expecting back a JSONRPCResponse
|
||||
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
|
||||
require.Equal(t, "", res.Header.Get("Cache-control"))
|
||||
|
||||
_, err = io.ReadAll(res.Body)
|
||||
|
||||
res.Body.Close()
|
||||
require.Nil(t, err, "reading from the body should not give back an error")
|
||||
|
||||
// send a request with default height, but as empty set of parameters.
|
||||
body = strings.NewReader(`{"jsonrpc": "2.0","method":"block","id": 0, "params": []}`)
|
||||
req, _ = http.NewRequest("Get", "http://localhost/", body)
|
||||
rec = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
|
||||
// Always expecting back a JSONRPCResponse
|
||||
require.True(t, statusOK(res.StatusCode), "should always return 2XX")
|
||||
require.Equal(t, "", res.Header.Get("Cache-control"))
|
||||
|
||||
_, err = io.ReadAll(res.Body)
|
||||
|
||||
res.Body.Close()
|
||||
require.Nil(t, err, "reading from the body should not give back an error")
|
||||
}
|
||||
|
||||
@@ -117,6 +117,22 @@ func WriteRPCResponseHTTPError(
|
||||
|
||||
// WriteRPCResponseHTTP marshals res as JSON (with indent) and writes it to w.
|
||||
func WriteRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error {
|
||||
return writeRPCResponseHTTP(w, []httpHeader{}, res...)
|
||||
}
|
||||
|
||||
// WriteCacheableRPCResponseHTTP marshals res as JSON (with indent) and writes
|
||||
// it to w. Adds cache-control to the response header and sets the expiry to
|
||||
// one day.
|
||||
func WriteCacheableRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error {
|
||||
return writeRPCResponseHTTP(w, []httpHeader{{"Cache-Control", "public, max-age=86400"}}, res...)
|
||||
}
|
||||
|
||||
type httpHeader struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
func writeRPCResponseHTTP(w http.ResponseWriter, headers []httpHeader, res ...types.RPCResponse) error {
|
||||
var v interface{}
|
||||
if len(res) == 1 {
|
||||
v = res[0]
|
||||
@@ -129,6 +145,9 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res ...types.RPCResponse) error
|
||||
return fmt.Errorf("json marshal: %w", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
for _, header := range headers {
|
||||
w.Header().Set(header.name, header.value)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
_, err = w.Write(jsonBytes)
|
||||
return err
|
||||
@@ -166,7 +185,6 @@ func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler
|
||||
// Without this, Chrome & Firefox were retrying aborted ajax requests,
|
||||
// at least to my localhost.
|
||||
if e := recover(); e != nil {
|
||||
|
||||
// If RPCResponse
|
||||
if res, ok := e.(types.RPCResponse); ok {
|
||||
if wErr := WriteRPCResponseHTTP(rww, res); wErr != nil {
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestWriteRPCResponseHTTP(t *testing.T) {
|
||||
|
||||
// one argument
|
||||
w := httptest.NewRecorder()
|
||||
err := WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(id, &sampleResult{"hello"}))
|
||||
err := WriteCacheableRPCResponseHTTP(w, types.NewRPCSuccessResponse(id, &sampleResult{"hello"}))
|
||||
require.NoError(t, err)
|
||||
resp := w.Result()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
@@ -120,6 +120,7 @@ func TestWriteRPCResponseHTTP(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
||||
assert.Equal(t, "public, max-age=86400", resp.Header.Get("Cache-control"))
|
||||
assert.Equal(t, `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": -1,
|
||||
|
||||
@@ -63,7 +63,14 @@ func makeHTTPHandler(rpcFunc *RPCFunc, logger log.Logger) func(http.ResponseWrit
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(dummyID, result)); err != nil {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -23,40 +23,96 @@ func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc, logger lo
|
||||
mux.HandleFunc("/", handleInvalidJSONRPCPaths(makeJSONRPCHandler(funcMap, logger)))
|
||||
}
|
||||
|
||||
// Function introspection
|
||||
type Option func(*RPCFunc)
|
||||
|
||||
// Cacheable enables returning a cache control header from RPC functions to
|
||||
// which it is applied.
|
||||
//
|
||||
// `noCacheDefArgs` is a list of argument names that, if omitted or set to
|
||||
// their defaults when calling the RPC function, will skip the response
|
||||
// caching.
|
||||
func Cacheable(noCacheDefArgs ...string) Option {
|
||||
return func(r *RPCFunc) {
|
||||
r.cacheable = true
|
||||
r.noCacheDefArgs = make(map[string]interface{})
|
||||
for _, arg := range noCacheDefArgs {
|
||||
r.noCacheDefArgs[arg] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ws enables WebSocket communication.
|
||||
func Ws() Option {
|
||||
return func(r *RPCFunc) {
|
||||
r.ws = true
|
||||
}
|
||||
}
|
||||
|
||||
// RPCFunc contains the introspected type information for a function
|
||||
type RPCFunc struct {
|
||||
f reflect.Value // underlying rpc function
|
||||
args []reflect.Type // type of each function arg
|
||||
returns []reflect.Type // type of each return arg
|
||||
argNames []string // name of each argument
|
||||
ws bool // websocket only
|
||||
f reflect.Value // underlying rpc function
|
||||
args []reflect.Type // type of each function arg
|
||||
returns []reflect.Type // type of each return arg
|
||||
argNames []string // name of each argument
|
||||
cacheable bool // enable cache control
|
||||
ws bool // enable websocket communication
|
||||
noCacheDefArgs map[string]interface{} // a lookup table of args that, if not supplied or are set to default values, cause us to not cache
|
||||
}
|
||||
|
||||
// NewRPCFunc wraps a function for introspection.
|
||||
// f is the function, args are comma separated argument names
|
||||
func NewRPCFunc(f interface{}, args string) *RPCFunc {
|
||||
return newRPCFunc(f, args, false)
|
||||
func NewRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
|
||||
return newRPCFunc(f, args, options...)
|
||||
}
|
||||
|
||||
// NewWSRPCFunc wraps a function for introspection and use in the websockets.
|
||||
func NewWSRPCFunc(f interface{}, args string) *RPCFunc {
|
||||
return newRPCFunc(f, args, true)
|
||||
func NewWSRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
|
||||
options = append(options, Ws())
|
||||
return newRPCFunc(f, args, options...)
|
||||
}
|
||||
|
||||
func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc {
|
||||
// cacheableWithArgs returns whether or not a call to this function is cacheable,
|
||||
// given the specified arguments.
|
||||
func (f *RPCFunc) cacheableWithArgs(args []reflect.Value) bool {
|
||||
if !f.cacheable {
|
||||
return false
|
||||
}
|
||||
// Skip the context variable common to all RPC functions
|
||||
for i := 1; i < len(f.args); i++ {
|
||||
// f.argNames does not include the context variable
|
||||
argName := f.argNames[i-1]
|
||||
if _, hasDefault := f.noCacheDefArgs[argName]; hasDefault {
|
||||
// Argument with default value was not supplied
|
||||
if i >= len(args) {
|
||||
return false
|
||||
}
|
||||
// Argument with default value is set to its zero value
|
||||
if args[i].IsZero() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newRPCFunc(f interface{}, args string, options ...Option) *RPCFunc {
|
||||
var argNames []string
|
||||
if args != "" {
|
||||
argNames = strings.Split(args, ",")
|
||||
}
|
||||
return &RPCFunc{
|
||||
|
||||
r := &RPCFunc{
|
||||
f: reflect.ValueOf(f),
|
||||
args: funcArgTypes(f),
|
||||
returns: funcReturnTypes(f),
|
||||
argNames: argNames,
|
||||
ws: ws,
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// return a function's argument types
|
||||
|
||||
@@ -216,6 +216,9 @@ paths:
|
||||
Please refer to
|
||||
https://docs.tendermint.com/v0.34/tendermint-core/using-tendermint.html#formatting
|
||||
for formatting/encoding rules.
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
parameters:
|
||||
- in: query
|
||||
name: tx
|
||||
@@ -623,9 +626,12 @@ paths:
|
||||
tags:
|
||||
- Info
|
||||
description: |
|
||||
Get block headers for minHeight <= height maxHeight.
|
||||
Get block headers for minHeight <= height <= maxHeight.
|
||||
|
||||
At most 20 items will be returned.
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Block headers, returned in descending order (highest first).
|
||||
@@ -655,6 +661,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get Block.
|
||||
|
||||
If the `height` field is set to a non-default value, upon success, the
|
||||
`Cache-Control` header will be set with the default maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Block informations.
|
||||
@@ -684,6 +693,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get Block By Hash.
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Block informations.
|
||||
@@ -704,7 +716,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: height
|
||||
description: height to return. If no height is provided, it will fetch informations regarding the latest block.
|
||||
description: height to return. If no height is provided, it will fetch information regarding the latest block.
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
@@ -714,6 +726,9 @@ paths:
|
||||
description: |
|
||||
Get block_results. When the `discard_abci_responses` storage flag is
|
||||
enabled, this endpoint will return an error.
|
||||
|
||||
If the `height` field is set to a non-default value, upon success, the
|
||||
`Cache-Control` header will be set with the default maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Block results.
|
||||
@@ -743,6 +758,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get Commit.
|
||||
|
||||
If the `height` field is set to a non-default value, upon success, the
|
||||
`Cache-Control` header will be set with the default maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
@@ -791,6 +809,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get Validators. Validators are sorted by voting power.
|
||||
|
||||
If the `height` field is set to a non-default value, upon success, the
|
||||
`Cache-Control` header will be set with the default maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Commit results.
|
||||
@@ -812,6 +833,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get genesis.
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Genesis results.
|
||||
@@ -890,6 +914,9 @@ paths:
|
||||
- Info
|
||||
description: |
|
||||
Get consensus parameters.
|
||||
|
||||
If the `height` field is set to a non-default value, upon success, the
|
||||
`Cache-Control` header will be set with the default maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: consensus parameters results.
|
||||
@@ -1080,14 +1107,14 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: hash
|
||||
description: transaction Hash to retrive
|
||||
description: hash of transaction to retrieve
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED"
|
||||
- in: query
|
||||
name: prove
|
||||
description: Include proofs of the transactions inclusion in the block
|
||||
description: Include proofs of the transaction's inclusion in the block
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
@@ -1096,7 +1123,10 @@ paths:
|
||||
tags:
|
||||
- Info
|
||||
description: |
|
||||
Get a trasasction
|
||||
Get a transaction
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Get a transaction`
|
||||
@@ -1112,12 +1142,15 @@ paths:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
/abci_info:
|
||||
get:
|
||||
summary: Get some info about the application.
|
||||
summary: Get info about the application.
|
||||
operationId: abci_info
|
||||
tags:
|
||||
- ABCI
|
||||
description: |
|
||||
Get some info about the application.
|
||||
Get info about the application.
|
||||
|
||||
Upon success, the `Cache-Control` header will be set with the default
|
||||
maximum age.
|
||||
responses:
|
||||
"200":
|
||||
description: Get some info about the application.
|
||||
|
||||
Reference in New Issue
Block a user