diff --git a/CHANGELOG.md b/CHANGELOG.md index 018c749326..aef7de410c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * x/distribution can now utilize an externally managed community pool. NOTE: this will make the message handlers for FundCommunityPool and CommunityPoolSpend error, as well as the query handler for CommunityPool. * (client) [#18101](https://github.com/cosmos/cosmos-sdk/pull/18101) Add a `keyring-default-keyname` in `client.toml` for specifying a default key name, and skip the need to use the `--from` flag when signing transactions. * (x/gov) [#24355](https://github.com/cosmos/cosmos-sdk/pull/24355) Allow users to set a custom CalculateVoteResultsAndVotingPower function to be used in govkeeper.Tally. +* (api) [#24428](https://github.com/cosmos/cosmos-sdk/pull/24428) Add block height to response headers ### Improvements diff --git a/server/api/server.go b/server/api/server.go index 84a0ad6bd8..31b0541782 100644 --- a/server/api/server.go +++ b/server/api/server.go @@ -11,6 +11,7 @@ import ( tmrpcserver "github.com/cometbft/cometbft/rpc/jsonrpc/server" gateway "github.com/cosmos/gogogateway" + "github.com/golang/protobuf/proto" //nolint:staticcheck // grpc-gateway uses deprecated golang/protobuf "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -84,11 +85,23 @@ func New(clientCtx client.Context, logger log.Logger, grpcSrv *grpc.Server) *Ser // Custom header matcher for mapping request headers to // GRPC metadata runtime.WithIncomingHeaderMatcher(CustomGRPCHeaderMatcher), + + // extension to set custom response headers + runtime.WithForwardResponseOption(customGRPCResponseHeaders), ), GRPCSrv: grpcSrv, } } +func customGRPCResponseHeaders(ctx context.Context, w http.ResponseWriter, _ proto.Message) error { + if meta, ok := runtime.ServerMetadataFromContext(ctx); ok { + if values := meta.HeaderMD.Get(grpctypes.GRPCBlockHeightHeader); len(values) == 1 { + w.Header().Set(grpctypes.GRPCBlockHeightHeader, values[0]) + } + } + return nil +} + // Start starts the API server. Internally, the API server leverages CometBFT's // JSON RPC server. Configuration options are provided via config.APIConfig // and are delegated to the CometBFT JSON RPC server. diff --git a/tests/e2e/distribution/grpc_query_suite.go b/tests/e2e/distribution/grpc_query_suite.go index b544e15200..3a33e82e26 100644 --- a/tests/e2e/distribution/grpc_query_suite.go +++ b/tests/e2e/distribution/grpc_query_suite.go @@ -3,6 +3,9 @@ package distribution import ( "encoding/json" "fmt" + "io" + "net/http" + "strconv" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/suite" @@ -506,3 +509,60 @@ func (s *GRPCQueryTestSuite) TestQueryValidatorCommunityPoolGRPC() { }) } } + +func (s *GRPCQueryTestSuite) TestQueryResponseMeta() { + val := s.network.Validators[0] + baseURL := val.APIAddress + startHeight, err := s.network.LatestHeight() + s.Require().NoError(err) + // wait 1 block to ensure state is committed + s.Require().NoError(s.network.WaitForNextBlock()) + // when + queryURL := fmt.Sprintf("%s/cosmos/distribution/v1beta1/validators/%s", baseURL, val.ValAddress.String()) + _, headers, err := doRequest(queryURL, map[string]string{}) + // then latest height is used + s.Require().NoError(err) + const heightRespHeaderKey = "X-Cosmos-Block-Height" + s.Require().Contains(headers, heightRespHeaderKey) + gotHeight, err := strconv.Atoi(headers[heightRespHeaderKey][0]) + s.Require().NoError(err) + s.Assert().GreaterOrEqual(gotHeight, int(startHeight)) + + // and when called with height header + _, headers, err = doRequest(queryURL, map[string]string{"X-Cosmos-Block-Height": strconv.Itoa(int(startHeight))}) + // then + s.Require().NoError(err) + s.Require().Contains(headers, heightRespHeaderKey) + gotHeight, err = strconv.Atoi(headers[heightRespHeaderKey][0]) + s.Require().NoError(err) + s.Assert().Equal(int(startHeight), gotHeight) +} + +func doRequest(url string, headers map[string]string) ([]byte, http.Header, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + client := &http.Client{} + + for key, value := range headers { + req.Header.Set(key, value) + } + + res, err := client.Do(req) + if err != nil { + return nil, nil, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, nil, err + } + + if err = res.Body.Close(); err != nil { + return nil, nil, err + } + fmt.Printf("headers: %v\n", res.Header) + return body, res.Header, nil +}