Merge PR #4777: Fix Height Queries

This commit is contained in:
Jack Zampolin 2019-07-25 17:45:13 -07:00 committed by Alexander Bezobchuk
parent b4ff0a11f6
commit 0ba74bb4b7
6 changed files with 76 additions and 60 deletions

View File

@ -0,0 +1,2 @@
All REST responses now wrap the original resource/result. The response
will contain two fields: height and result.

View File

@ -0,0 +1 @@
Return height in responses when querying against BaseApp

View File

@ -463,6 +463,7 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc
return abci.ResponseQuery{ return abci.ResponseQuery{
Code: uint32(sdk.CodeOK), Code: uint32(sdk.CodeOK),
Codespace: string(sdk.CodespaceRoot), Codespace: string(sdk.CodespaceRoot),
Height: req.Height,
Value: []byte(app.appVersion), Value: []byte(app.appVersion),
} }
@ -474,6 +475,7 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc
return abci.ResponseQuery{ return abci.ResponseQuery{
Code: uint32(sdk.CodeOK), Code: uint32(sdk.CodeOK),
Codespace: string(sdk.CodespaceRoot), Codespace: string(sdk.CodespaceRoot),
Height: req.Height,
Value: value, Value: value,
} }
} }
@ -482,7 +484,7 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc
return sdk.ErrUnknownRequest(msg).QueryResult() return sdk.ErrUnknownRequest(msg).QueryResult()
} }
func handleQueryStore(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { func handleQueryStore(app *BaseApp, path []string, req abci.RequestQuery) abci.ResponseQuery {
// "/store" prefix for store queries // "/store" prefix for store queries
queryable, ok := app.cms.(sdk.Queryable) queryable, ok := app.cms.(sdk.Queryable)
if !ok { if !ok {
@ -491,7 +493,11 @@ func handleQueryStore(app *BaseApp, path []string, req abci.RequestQuery) (res a
} }
req.Path = "/" + strings.Join(path[1:], "/") req.Path = "/" + strings.Join(path[1:], "/")
return queryable.Query(req)
resp := queryable.Query(req)
resp.Height = req.Height
return resp
} }
func handleQueryP2P(app *BaseApp, path []string, _ abci.RequestQuery) (res abci.ResponseQuery) { func handleQueryP2P(app *BaseApp, path []string, _ abci.RequestQuery) (res abci.ResponseQuery) {
@ -503,9 +509,11 @@ func handleQueryP2P(app *BaseApp, path []string, _ abci.RequestQuery) (res abci.
switch typ { switch typ {
case "addr": case "addr":
return app.FilterPeerByAddrPort(arg) return app.FilterPeerByAddrPort(arg)
case "id": case "id":
return app.FilterPeerByID(arg) return app.FilterPeerByID(arg)
} }
default: default:
msg := "Expected second parameter to be filter" msg := "Expected second parameter to be filter"
return sdk.ErrUnknownRequest(msg).QueryResult() return sdk.ErrUnknownRequest(msg).QueryResult()
@ -554,13 +562,15 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res
return abci.ResponseQuery{ return abci.ResponseQuery{
Code: uint32(err.Code()), Code: uint32(err.Code()),
Codespace: string(err.Codespace()), Codespace: string(err.Codespace()),
Height: req.Height,
Log: err.ABCILog(), Log: err.ABCILog(),
} }
} }
return abci.ResponseQuery{ return abci.ResponseQuery{
Code: uint32(sdk.CodeOK), Code: uint32(sdk.CodeOK),
Value: resBytes, Height: req.Height,
Value: resBytes,
} }
} }

View File

@ -81,6 +81,16 @@ func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, height i
return res, height, err return res, height, err
} }
// When a client did not provide a query height, manually query for it so it can
// be injected downstream into responses.
if ctx.Height == 0 {
status, err := node.Status()
if err != nil {
return res, height, err
}
ctx = ctx.WithHeight(status.SyncInfo.LatestBlockHeight)
}
opts := rpcclient.ABCIQueryOptions{ opts := rpcclient.ABCIQueryOptions{
Height: ctx.Height, Height: ctx.Height,
Prove: !ctx.TrustNode, Prove: !ctx.TrustNode,

View File

@ -24,6 +24,21 @@ const (
DefaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 DefaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
) )
// ResponseWithHeight defines a response object type that wraps an original
// response with a height.
type ResponseWithHeight struct {
Height int64 `json:"height"`
Result json.RawMessage `json:"result"`
}
// NewResponseWithHeight creates a new ResponseWithHeight instance
func NewResponseWithHeight(height int64, result json.RawMessage) ResponseWithHeight {
return ResponseWithHeight{
Height: height,
Result: result,
}
}
// GasEstimateResponse defines a response definition for tx gas estimation. // GasEstimateResponse defines a response definition for tx gas estimation.
type GasEstimateResponse struct { type GasEstimateResponse struct {
GasEstimate uint64 `json:"gas_estimate"` GasEstimate uint64 `json:"gas_estimate"`
@ -222,28 +237,27 @@ func ParseQueryHeightOrReturnBadRequest(w http.ResponseWriter, cliCtx context.CL
return cliCtx, true return cliCtx, true
} }
// PostProcessResponse performs post processing for a REST response. // PostProcessResponse performs post processing for a REST response. The result
// If the height is greater than zero it will be injected into the body // returned to clients will contain two fields, the height at which the resource
// of the response. An internal server error is written to the response // was queried at and the original result.
// if the height is negative or an encoding/decoding error occurs. func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, resp interface{}) {
func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, response interface{}) { var result []byte
var output []byte
if cliCtx.Height < 0 { if cliCtx.Height < 0 {
WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("negative height in response").Error()) WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("negative height in response").Error())
return return
} }
switch response.(type) { switch resp.(type) {
case []byte: case []byte:
output = response.([]byte) result = resp.([]byte)
default: default:
var err error var err error
if cliCtx.Indent { if cliCtx.Indent {
output, err = cliCtx.Codec.MarshalJSONIndent(response, "", " ") result, err = cliCtx.Codec.MarshalJSONIndent(resp, "", " ")
} else { } else {
output, err = cliCtx.Codec.MarshalJSON(response) result, err = cliCtx.Codec.MarshalJSON(resp)
} }
if err != nil { if err != nil {
@ -252,29 +266,22 @@ func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, respo
} }
} }
// inject the height into the response by: wrappedResp := NewResponseWithHeight(cliCtx.Height, result)
// - decoding into a map
// - adding the height to the map
// - encoding using standard JSON library
if cliCtx.Height > 0 {
m := make(map[string]interface{})
err := json.Unmarshal(output, &m)
if err != nil {
WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
m["height"] = cliCtx.Height var (
output []byte
err error
)
if cliCtx.Indent { if cliCtx.Indent {
output, err = json.MarshalIndent(m, "", " ") output, err = cliCtx.Codec.MarshalJSONIndent(wrappedResp, "", " ")
} else { } else {
output, err = json.Marshal(m) output, err = cliCtx.Codec.MarshalJSON(wrappedResp)
} }
if err != nil {
WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) if err != nil {
return WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
} return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@ -3,12 +3,10 @@
package rest package rest
import ( import (
"encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -148,6 +146,7 @@ func TestProcessPostResponse(t *testing.T) {
// mock account // mock account
// PubKey field ensures amino encoding is used first since standard // PubKey field ensures amino encoding is used first since standard
// JSON encoding will panic on crypto.PubKey // JSON encoding will panic on crypto.PubKey
type mockAccount struct { type mockAccount struct {
Address types.AccAddress `json:"address"` Address types.AccAddress `json:"address"`
Coins types.Coins `json:"coins"` Coins types.Coins `json:"coins"`
@ -173,23 +172,17 @@ func TestProcessPostResponse(t *testing.T) {
cdc.RegisterConcrete(&mockAccount{}, "cosmos-sdk/mockAccount", nil) cdc.RegisterConcrete(&mockAccount{}, "cosmos-sdk/mockAccount", nil)
ctx = ctx.WithCodec(cdc) ctx = ctx.WithCodec(cdc)
// setup expected json responses with zero height // setup expected results
jsonNoHeight, err := cdc.MarshalJSON(acc) jsonNoIndent, err := ctx.Codec.MarshalJSON(acc)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, jsonNoHeight) jsonWithIndent, err := ctx.Codec.MarshalJSONIndent(acc, "", " ")
jsonIndentNoHeight, err := cdc.MarshalJSONIndent(acc, "", " ")
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, jsonIndentNoHeight) respNoIndent := NewResponseWithHeight(height, jsonNoIndent)
respWithIndent := NewResponseWithHeight(height, jsonWithIndent)
// decode into map to order alphabetically expectedNoIndent, err := ctx.Codec.MarshalJSON(respNoIndent)
m := make(map[string]interface{})
err = json.Unmarshal(jsonNoHeight, &m)
require.Nil(t, err) require.Nil(t, err)
jsonMap, err := json.Marshal(m) expectedWithIndent, err := ctx.Codec.MarshalJSONIndent(respWithIndent, "", " ")
require.Nil(t, err) require.Nil(t, err)
jsonWithHeight := append(append([]byte(`{"height":`), []byte(strconv.Itoa(int(height))+",")...), jsonMap[1:]...)
jsonIndentMap, err := json.MarshalIndent(m, "", " ")
jsonIndentWithHeight := append(append([]byte(`{`+"\n "+` "height": `), []byte(strconv.Itoa(int(height))+",")...), jsonIndentMap[1:]...)
// check that negative height writes an error // check that negative height writes an error
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -197,24 +190,17 @@ func TestProcessPostResponse(t *testing.T) {
PostProcessResponse(w, ctx, acc) PostProcessResponse(w, ctx, acc)
require.Equal(t, http.StatusInternalServerError, w.Code) require.Equal(t, http.StatusInternalServerError, w.Code)
// check that zero height returns expected response
ctx = ctx.WithHeight(0)
runPostProcessResponse(t, ctx, acc, jsonNoHeight, false)
// check zero height with indent
runPostProcessResponse(t, ctx, acc, jsonIndentNoHeight, true)
// check that height returns expected response // check that height returns expected response
ctx = ctx.WithHeight(height) ctx = ctx.WithHeight(height)
runPostProcessResponse(t, ctx, acc, jsonWithHeight, false) runPostProcessResponse(t, ctx, acc, expectedNoIndent, false)
// check height with indent // check height with indent
runPostProcessResponse(t, ctx, acc, jsonIndentWithHeight, true) runPostProcessResponse(t, ctx, acc, expectedWithIndent, true)
} }
// asserts that ResponseRecorder returns the expected code and body // asserts that ResponseRecorder returns the expected code and body
// runs PostProcessResponse on the objects regular interface and on // runs PostProcessResponse on the objects regular interface and on
// the marshalled struct. // the marshalled struct.
func runPostProcessResponse(t *testing.T, ctx context.CLIContext, obj interface{}, func runPostProcessResponse(t *testing.T, ctx context.CLIContext, obj interface{}, expectedBody []byte, indent bool) {
expectedBody []byte, indent bool,
) {
if indent { if indent {
ctx.Indent = indent ctx.Indent = indent
} }