From 8ad4b564b23f0979e00819fa9c435ed58cde50a6 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:12:20 -0800 Subject: [PATCH 01/25] wip --- server/v2/api/grpcgateway/server.go | 2 +- server/v2/api/grpcgateway/tunnel.go | 50 ++++++++++++++++ server/v2/api/grpcgateway/tunnel_test.go | 21 +++++++ server/v2/api/grpcgateway/uri.go | 76 ++++++++++++++++++++++++ x/authz/register.go | 50 ++++++++++++++++ x/authz/register_test.go | 16 +++++ 6 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 server/v2/api/grpcgateway/tunnel.go create mode 100644 server/v2/api/grpcgateway/tunnel_test.go create mode 100644 server/v2/api/grpcgateway/uri.go create mode 100644 x/authz/register.go create mode 100644 x/authz/register_test.go diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 977eb369c894..fc36d227bb8e 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -76,7 +76,7 @@ func New[T transaction.Tx]( s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - mux.Handle("/", s.GRPCGatewayRouter) + mux.Handle("/", NewTunnel[T](s.GRPCGatewayRouter)) s.server = &http.Server{ Addr: s.config.Address, diff --git a/server/v2/api/grpcgateway/tunnel.go b/server/v2/api/grpcgateway/tunnel.go new file mode 100644 index 000000000000..7d1010439a62 --- /dev/null +++ b/server/v2/api/grpcgateway/tunnel.go @@ -0,0 +1,50 @@ +package grpcgateway + +import ( + "net/http" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/server/v2/appmanager" +) + +var _ http.Handler = &Tunnel[transaction.Tx]{} + +type Tunnel[T transaction.Tx] struct { + // gateway is the fallback grpc gateway mux handler. + gateway *runtime.ServeMux + + // customEndpointMapping is a mapping of custom GET options on proto RPC handlers, to the fully qualified method name. + // + // example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata + customEndpointMapping map[string]string + + appManager appmanager.AppManager[T] +} + +func NewTunnel[T transaction.Tx](gateway *runtime.ServeMux) *Tunnel[T] { + return &Tunnel[T]{gateway: gateway} +} + +// Handle some things: +// +// - see if we can match the request to +func (t *Tunnel[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + uri := request.URL.RequestURI() + uriMatch := matchURI(uri, t.customEndpointMapping) + if uriMatch != nil { + if uriMatch.HasParams() { + + } else { + + } + } else { + t.gateway.ServeHTTP(writer, request) + } +} + +func createMessage(method string) (gogoproto.Message, error) { + return nil, nil +} diff --git a/server/v2/api/grpcgateway/tunnel_test.go b/server/v2/api/grpcgateway/tunnel_test.go new file mode 100644 index 000000000000..e7095f7045ed --- /dev/null +++ b/server/v2/api/grpcgateway/tunnel_test.go @@ -0,0 +1,21 @@ +package grpcgateway + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestThing(t *testing.T) { + bz, err := os.ReadFile("mapping.json") + require.NoError(t, err) + var mapping map[string]string + err = json.Unmarshal(bz, &mapping) + require.NoError(t, err) + + match := matchURI("/cosmos/bank/v1beta1/denom_owners/ibc/denom/1", mapping) + fmt.Println(match) +} diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go new file mode 100644 index 000000000000..faaa2b95be66 --- /dev/null +++ b/server/v2/api/grpcgateway/uri.go @@ -0,0 +1,76 @@ +package grpcgateway + +import ( + "regexp" + "strings" +) + +// URIMatch contains the matching results +type URIMatch struct { + MethodName string + Params map[string]string +} + +func (uri URIMatch) HasParams() bool { + return len(uri.Params) > 0 +} + +// matchURI checks if a given URI matches any pattern and extracts wildcard values +func matchURI(uri string, patterns map[string]string) *URIMatch { + // Remove trailing slash if present + uri = strings.TrimRight(uri, "/") + + for pattern, methodName := range patterns { + // Remove trailing slash from pattern if present + pattern = strings.TrimRight(pattern, "/") + + // Get regex pattern and param names + regexPattern, paramNames := patternToRegex(pattern) + + // Compile and match + regex := regexp.MustCompile(regexPattern) + matches := regex.FindStringSubmatch(uri) + + if matches != nil && len(matches) > 1 { + // First match is the full string, subsequent matches are capture groups + params := make(map[string]string) + for i, name := range paramNames { + params[name] = matches[i+1] + } + + return &URIMatch{ + MethodName: methodName, + Params: params, + } + } + } + + return nil +} + +// patternToRegex converts a URI pattern with wildcards to a regex pattern +// Returns the regex pattern and a slice of parameter names in order +func patternToRegex(pattern string) (string, []string) { + escaped := regexp.QuoteMeta(pattern) + var paramNames []string + + // Extract and replace {param=**} patterns + r1 := regexp.MustCompile(`\\\{([^}]+?)=\\\*\\\*\\\}`) + escaped = r1.ReplaceAllStringFunc(escaped, func(match string) string { + // Extract param name without the =** suffix + name := regexp.MustCompile(`\\\{(.+?)=`).FindStringSubmatch(match)[1] + paramNames = append(paramNames, name) + return "(.+)" + }) + + // Extract and replace {param} patterns + r2 := regexp.MustCompile(`\\\{([^}]+)\\\}`) + escaped = r2.ReplaceAllStringFunc(escaped, func(match string) string { + // Extract param name from between { and } + name := regexp.MustCompile(`\\\{(.*?)\\\}`).FindStringSubmatch(match)[1] + paramNames = append(paramNames, name) + return "([^/]+)" + }) + + return "^" + escaped + "$", paramNames +} diff --git a/x/authz/register.go b/x/authz/register.go new file mode 100644 index 000000000000..bacc40af4f40 --- /dev/null +++ b/x/authz/register.go @@ -0,0 +1,50 @@ +package authz + +import ( + "fmt" + + gogoproto "github.com/cosmos/gogoproto/proto" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// GetProtoHTTPGetRuleMapping returns a mapping of proto method full name to it's HTTP GET annotation. +func GetProtoHTTPGetRuleMapping() map[string]string { + protoFiles, err := gogoproto.MergedRegistry() + if err != nil { + panic(err) + } + + httpGets := make(map[string]string) + protoFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Services().Len(); i++ { + // Get the service descriptor + sd := fd.Services().Get(i) + + for j := 0; j < sd.Methods().Len(); j++ { + // Get the method descriptor + md := sd.Methods().Get(j) + + httpOption := proto.GetExtension(md.Options(), annotations.E_Http) + if httpOption == nil { + continue + } + + httpRule, ok := httpOption.(*annotations.HttpRule) + if !ok || httpRule == nil { + continue + } + if httpRule.GetGet() == "" { + continue + } + + httpGets[httpRule.GetGet()] = string(md.FullName()) + fmt.Printf("service: %q \t get option: %q\n", md.FullName(), httpRule.GetGet()) + } + } + return true + }) + + return httpGets +} diff --git a/x/authz/register_test.go b/x/authz/register_test.go new file mode 100644 index 000000000000..8abd565c766d --- /dev/null +++ b/x/authz/register_test.go @@ -0,0 +1,16 @@ +package authz + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRegister(t *testing.T) { + m := GetProtoHTTPGetRuleMapping() + bz, err := json.Marshal(m) + require.NoError(t, err) + os.WriteFile("mapping.json", bz, 0600) +} From 0bba48aada46b24485845d976e005ec98cb4f5e6 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:12:25 -0800 Subject: [PATCH 02/25] wip --- server/v2/api/grpcgateway/mapping.json | 1 + x/authz/mapping.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 server/v2/api/grpcgateway/mapping.json create mode 100644 x/authz/mapping.json diff --git a/server/v2/api/grpcgateway/mapping.json b/server/v2/api/grpcgateway/mapping.json new file mode 100644 index 000000000000..6f2edad37300 --- /dev/null +++ b/server/v2/api/grpcgateway/mapping.json @@ -0,0 +1 @@ +{"/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo","/cosmos/auth/v1beta1/accounts":"cosmos.auth.v1beta1.Query.Accounts","/cosmos/auth/v1beta1/accounts/{address}":"cosmos.auth.v1beta1.Query.Account","/cosmos/auth/v1beta1/address_by_id/{account_id}":"cosmos.auth.v1beta1.Query.AccountAddressByID","/cosmos/auth/v1beta1/bech32":"cosmos.auth.v1beta1.Query.Bech32Prefix","/cosmos/auth/v1beta1/bech32/{address_bytes}":"cosmos.auth.v1beta1.Query.AddressBytesToString","/cosmos/auth/v1beta1/bech32/{address_string}":"cosmos.auth.v1beta1.Query.AddressStringToBytes","/cosmos/auth/v1beta1/module_accounts":"cosmos.auth.v1beta1.Query.ModuleAccounts","/cosmos/auth/v1beta1/module_accounts/{name}":"cosmos.auth.v1beta1.Query.ModuleAccountByName","/cosmos/auth/v1beta1/params":"cosmos.auth.v1beta1.Query.Params","/cosmos/authz/v1beta1/grants":"cosmos.authz.v1beta1.Query.Grants","/cosmos/authz/v1beta1/grants/grantee/{grantee}":"cosmos.authz.v1beta1.Query.GranteeGrants","/cosmos/authz/v1beta1/grants/granter/{granter}":"cosmos.authz.v1beta1.Query.GranterGrants","/cosmos/bank/v1beta1/balances/{address}":"cosmos.bank.v1beta1.Query.AllBalances","/cosmos/bank/v1beta1/balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.Balance","/cosmos/bank/v1beta1/denom_owners/{denom=**}":"cosmos.bank.v1beta1.Query.DenomOwners","/cosmos/bank/v1beta1/denom_owners_by_query":"cosmos.bank.v1beta1.Query.DenomOwnersByQuery","/cosmos/bank/v1beta1/denoms_metadata":"cosmos.bank.v1beta1.Query.DenomsMetadata","/cosmos/bank/v1beta1/denoms_metadata/{denom=**}":"cosmos.bank.v1beta1.Query.DenomMetadata","/cosmos/bank/v1beta1/denoms_metadata_by_query_string":"cosmos.bank.v1beta1.Query.DenomMetadataByQueryString","/cosmos/bank/v1beta1/params":"cosmos.bank.v1beta1.Query.Params","/cosmos/bank/v1beta1/send_enabled":"cosmos.bank.v1beta1.Query.SendEnabled","/cosmos/bank/v1beta1/spendable_balances/{address}":"cosmos.bank.v1beta1.Query.SpendableBalances","/cosmos/bank/v1beta1/spendable_balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.SpendableBalanceByDenom","/cosmos/bank/v1beta1/supply":"cosmos.bank.v1beta1.Query.TotalSupply","/cosmos/bank/v1beta1/supply/by_denom":"cosmos.bank.v1beta1.Query.SupplyOf","/cosmos/staking/v1beta1/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.DelegatorDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations":"cosmos.staking.v1beta1.Query.Redelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.DelegatorUnbondingDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators":"cosmos.staking.v1beta1.Query.DelegatorValidators","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.DelegatorValidator","/cosmos/staking/v1beta1/historical_info/{height}":"cosmos.staking.v1beta1.Query.HistoricalInfo","/cosmos/staking/v1beta1/params":"cosmos.staking.v1beta1.Query.Params","/cosmos/staking/v1beta1/pool":"cosmos.staking.v1beta1.Query.Pool","/cosmos/staking/v1beta1/validators":"cosmos.staking.v1beta1.Query.Validators","/cosmos/staking/v1beta1/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.Validator","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations":"cosmos.staking.v1beta1.Query.ValidatorDelegations","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.Delegation","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation":"cosmos.staking.v1beta1.Query.UnbondingDelegation","/cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.ValidatorUnbondingDelegations","/cosmos/tx/v1beta1/txs":"cosmos.tx.v1beta1.Service.GetTxsEvent","/cosmos/tx/v1beta1/txs/block/{height}":"cosmos.tx.v1beta1.Service.GetBlockWithTxs","/cosmos/tx/v1beta1/txs/{hash}":"cosmos.tx.v1beta1.Service.GetTx"} \ No newline at end of file diff --git a/x/authz/mapping.json b/x/authz/mapping.json new file mode 100644 index 000000000000..6f2edad37300 --- /dev/null +++ b/x/authz/mapping.json @@ -0,0 +1 @@ +{"/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo","/cosmos/auth/v1beta1/accounts":"cosmos.auth.v1beta1.Query.Accounts","/cosmos/auth/v1beta1/accounts/{address}":"cosmos.auth.v1beta1.Query.Account","/cosmos/auth/v1beta1/address_by_id/{account_id}":"cosmos.auth.v1beta1.Query.AccountAddressByID","/cosmos/auth/v1beta1/bech32":"cosmos.auth.v1beta1.Query.Bech32Prefix","/cosmos/auth/v1beta1/bech32/{address_bytes}":"cosmos.auth.v1beta1.Query.AddressBytesToString","/cosmos/auth/v1beta1/bech32/{address_string}":"cosmos.auth.v1beta1.Query.AddressStringToBytes","/cosmos/auth/v1beta1/module_accounts":"cosmos.auth.v1beta1.Query.ModuleAccounts","/cosmos/auth/v1beta1/module_accounts/{name}":"cosmos.auth.v1beta1.Query.ModuleAccountByName","/cosmos/auth/v1beta1/params":"cosmos.auth.v1beta1.Query.Params","/cosmos/authz/v1beta1/grants":"cosmos.authz.v1beta1.Query.Grants","/cosmos/authz/v1beta1/grants/grantee/{grantee}":"cosmos.authz.v1beta1.Query.GranteeGrants","/cosmos/authz/v1beta1/grants/granter/{granter}":"cosmos.authz.v1beta1.Query.GranterGrants","/cosmos/bank/v1beta1/balances/{address}":"cosmos.bank.v1beta1.Query.AllBalances","/cosmos/bank/v1beta1/balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.Balance","/cosmos/bank/v1beta1/denom_owners/{denom=**}":"cosmos.bank.v1beta1.Query.DenomOwners","/cosmos/bank/v1beta1/denom_owners_by_query":"cosmos.bank.v1beta1.Query.DenomOwnersByQuery","/cosmos/bank/v1beta1/denoms_metadata":"cosmos.bank.v1beta1.Query.DenomsMetadata","/cosmos/bank/v1beta1/denoms_metadata/{denom=**}":"cosmos.bank.v1beta1.Query.DenomMetadata","/cosmos/bank/v1beta1/denoms_metadata_by_query_string":"cosmos.bank.v1beta1.Query.DenomMetadataByQueryString","/cosmos/bank/v1beta1/params":"cosmos.bank.v1beta1.Query.Params","/cosmos/bank/v1beta1/send_enabled":"cosmos.bank.v1beta1.Query.SendEnabled","/cosmos/bank/v1beta1/spendable_balances/{address}":"cosmos.bank.v1beta1.Query.SpendableBalances","/cosmos/bank/v1beta1/spendable_balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.SpendableBalanceByDenom","/cosmos/bank/v1beta1/supply":"cosmos.bank.v1beta1.Query.TotalSupply","/cosmos/bank/v1beta1/supply/by_denom":"cosmos.bank.v1beta1.Query.SupplyOf","/cosmos/staking/v1beta1/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.DelegatorDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations":"cosmos.staking.v1beta1.Query.Redelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.DelegatorUnbondingDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators":"cosmos.staking.v1beta1.Query.DelegatorValidators","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.DelegatorValidator","/cosmos/staking/v1beta1/historical_info/{height}":"cosmos.staking.v1beta1.Query.HistoricalInfo","/cosmos/staking/v1beta1/params":"cosmos.staking.v1beta1.Query.Params","/cosmos/staking/v1beta1/pool":"cosmos.staking.v1beta1.Query.Pool","/cosmos/staking/v1beta1/validators":"cosmos.staking.v1beta1.Query.Validators","/cosmos/staking/v1beta1/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.Validator","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations":"cosmos.staking.v1beta1.Query.ValidatorDelegations","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.Delegation","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation":"cosmos.staking.v1beta1.Query.UnbondingDelegation","/cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.ValidatorUnbondingDelegations","/cosmos/tx/v1beta1/txs":"cosmos.tx.v1beta1.Service.GetTxsEvent","/cosmos/tx/v1beta1/txs/block/{height}":"cosmos.tx.v1beta1.Service.GetBlockWithTxs","/cosmos/tx/v1beta1/txs/{hash}":"cosmos.tx.v1beta1.Service.GetTx"} \ No newline at end of file From 35f6885c67749536c9c51248cfc77544e650865b Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:36:30 -0800 Subject: [PATCH 03/25] finalize setup of grpc gateway autoreg --- server/v2/api/grpcgateway/interceptor.go | 121 +++++++++++++++++++++++ server/v2/api/grpcgateway/mapping.json | 1 - server/v2/api/grpcgateway/server.go | 4 +- server/v2/api/grpcgateway/tunnel.go | 50 ---------- server/v2/api/grpcgateway/tunnel_test.go | 21 ---- server/v2/api/grpcgateway/uri.go | 110 +++++++++++++++++++-- simapp/v2/simdv2/cmd/commands.go | 8 +- x/authz/register.go | 50 ---------- x/authz/register_test.go | 16 --- 9 files changed, 225 insertions(+), 156 deletions(-) create mode 100644 server/v2/api/grpcgateway/interceptor.go delete mode 100644 server/v2/api/grpcgateway/mapping.json delete mode 100644 server/v2/api/grpcgateway/tunnel.go delete mode 100644 server/v2/api/grpcgateway/tunnel_test.go delete mode 100644 x/authz/register.go delete mode 100644 x/authz/register_test.go diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go new file mode 100644 index 000000000000..f6a6318619f4 --- /dev/null +++ b/server/v2/api/grpcgateway/interceptor.go @@ -0,0 +1,121 @@ +package grpcgateway + +import ( + "encoding/json" + "fmt" + "net/http" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/reflect/protoreflect" + + "google.golang.org/protobuf/proto" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/server/v2/appmanager" +) + +var _ http.Handler = &GatewayInterceptor[transaction.Tx]{} + +type GatewayInterceptor[T transaction.Tx] struct { + // gateway is the fallback grpc gateway mux handler. + gateway *runtime.ServeMux + + // customEndpointMapping is a mapping of custom GET options on proto RPC handlers, to the fully qualified method name. + // + // example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata + customEndpointMapping map[string]string + + // appManager is used to route queries through the SDK router. + appManager appmanager.AppManager[T] +} + +func NewGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) *GatewayInterceptor[T] { + return &GatewayInterceptor[T]{ + gateway: gateway, + customEndpointMapping: GetProtoHTTPGetRuleMapping(), + appManager: am, + } +} + +func (g *GatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + fmt.Println("printing mapping") + fmt.Println(g.customEndpointMapping) + fmt.Println("got request for: ", request.URL.Path) + uri := request.URL.RequestURI() + fmt.Println("checking URI: ", uri) + uriMatch := matchURI(uri, g.customEndpointMapping) + if uriMatch != nil { + fmt.Println("got match: ", uriMatch.QueryInputName) + var msg gogoproto.Message + var err error + + switch request.Method { + case http.MethodPost: + msg, err = createMessageFromJSON(uriMatch, request) + case http.MethodGet: + msg, err = createMessage(uriMatch) + default: + http.Error(writer, "unsupported http method", http.StatusMethodNotAllowed) + return + } + if err != nil { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + + query, err := g.appManager.Query(request.Context(), 0, msg) + if err != nil { + http.Error(writer, "Error querying", http.StatusInternalServerError) + return + } + writer.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(writer).Encode(query); err != nil { + http.Error(writer, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) + } + } else { + fmt.Println("no custom endpoint mapping found, falling back to gateway router") + g.gateway.ServeHTTP(writer, request) + } +} + +// GetProtoHTTPGetRuleMapping returns a mapping of proto method full name to it's HTTP GET annotation. +func GetProtoHTTPGetRuleMapping() map[string]string { + protoFiles, err := gogoproto.MergedRegistry() + if err != nil { + panic(err) + } + + httpGets := make(map[string]string) + protoFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Services().Len(); i++ { + // Get the service descriptor + sd := fd.Services().Get(i) + + for j := 0; j < sd.Methods().Len(); j++ { + // Get the method descriptor + md := sd.Methods().Get(j) + + httpOption := proto.GetExtension(md.Options(), annotations.E_Http) + if httpOption == nil { + continue + } + + httpRule, ok := httpOption.(*annotations.HttpRule) + if !ok || httpRule == nil { + continue + } + if httpRule.GetGet() == "" { + continue + } + + httpGets[httpRule.GetGet()] = string(md.Input().FullName()) + fmt.Printf("input name: %q \t get option: %q\n", md.Input().FullName(), httpRule.GetGet()) + } + } + return true + }) + + return httpGets +} diff --git a/server/v2/api/grpcgateway/mapping.json b/server/v2/api/grpcgateway/mapping.json deleted file mode 100644 index 6f2edad37300..000000000000 --- a/server/v2/api/grpcgateway/mapping.json +++ /dev/null @@ -1 +0,0 @@ -{"/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo","/cosmos/auth/v1beta1/accounts":"cosmos.auth.v1beta1.Query.Accounts","/cosmos/auth/v1beta1/accounts/{address}":"cosmos.auth.v1beta1.Query.Account","/cosmos/auth/v1beta1/address_by_id/{account_id}":"cosmos.auth.v1beta1.Query.AccountAddressByID","/cosmos/auth/v1beta1/bech32":"cosmos.auth.v1beta1.Query.Bech32Prefix","/cosmos/auth/v1beta1/bech32/{address_bytes}":"cosmos.auth.v1beta1.Query.AddressBytesToString","/cosmos/auth/v1beta1/bech32/{address_string}":"cosmos.auth.v1beta1.Query.AddressStringToBytes","/cosmos/auth/v1beta1/module_accounts":"cosmos.auth.v1beta1.Query.ModuleAccounts","/cosmos/auth/v1beta1/module_accounts/{name}":"cosmos.auth.v1beta1.Query.ModuleAccountByName","/cosmos/auth/v1beta1/params":"cosmos.auth.v1beta1.Query.Params","/cosmos/authz/v1beta1/grants":"cosmos.authz.v1beta1.Query.Grants","/cosmos/authz/v1beta1/grants/grantee/{grantee}":"cosmos.authz.v1beta1.Query.GranteeGrants","/cosmos/authz/v1beta1/grants/granter/{granter}":"cosmos.authz.v1beta1.Query.GranterGrants","/cosmos/bank/v1beta1/balances/{address}":"cosmos.bank.v1beta1.Query.AllBalances","/cosmos/bank/v1beta1/balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.Balance","/cosmos/bank/v1beta1/denom_owners/{denom=**}":"cosmos.bank.v1beta1.Query.DenomOwners","/cosmos/bank/v1beta1/denom_owners_by_query":"cosmos.bank.v1beta1.Query.DenomOwnersByQuery","/cosmos/bank/v1beta1/denoms_metadata":"cosmos.bank.v1beta1.Query.DenomsMetadata","/cosmos/bank/v1beta1/denoms_metadata/{denom=**}":"cosmos.bank.v1beta1.Query.DenomMetadata","/cosmos/bank/v1beta1/denoms_metadata_by_query_string":"cosmos.bank.v1beta1.Query.DenomMetadataByQueryString","/cosmos/bank/v1beta1/params":"cosmos.bank.v1beta1.Query.Params","/cosmos/bank/v1beta1/send_enabled":"cosmos.bank.v1beta1.Query.SendEnabled","/cosmos/bank/v1beta1/spendable_balances/{address}":"cosmos.bank.v1beta1.Query.SpendableBalances","/cosmos/bank/v1beta1/spendable_balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.SpendableBalanceByDenom","/cosmos/bank/v1beta1/supply":"cosmos.bank.v1beta1.Query.TotalSupply","/cosmos/bank/v1beta1/supply/by_denom":"cosmos.bank.v1beta1.Query.SupplyOf","/cosmos/staking/v1beta1/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.DelegatorDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations":"cosmos.staking.v1beta1.Query.Redelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.DelegatorUnbondingDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators":"cosmos.staking.v1beta1.Query.DelegatorValidators","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.DelegatorValidator","/cosmos/staking/v1beta1/historical_info/{height}":"cosmos.staking.v1beta1.Query.HistoricalInfo","/cosmos/staking/v1beta1/params":"cosmos.staking.v1beta1.Query.Params","/cosmos/staking/v1beta1/pool":"cosmos.staking.v1beta1.Query.Pool","/cosmos/staking/v1beta1/validators":"cosmos.staking.v1beta1.Query.Validators","/cosmos/staking/v1beta1/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.Validator","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations":"cosmos.staking.v1beta1.Query.ValidatorDelegations","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.Delegation","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation":"cosmos.staking.v1beta1.Query.UnbondingDelegation","/cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.ValidatorUnbondingDelegations","/cosmos/tx/v1beta1/txs":"cosmos.tx.v1beta1.Service.GetTxsEvent","/cosmos/tx/v1beta1/txs/block/{height}":"cosmos.tx.v1beta1.Service.GetBlockWithTxs","/cosmos/tx/v1beta1/txs/{hash}":"cosmos.tx.v1beta1.Service.GetTx"} \ No newline at end of file diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index fc36d227bb8e..d7fa58198082 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -14,6 +14,7 @@ import ( "cosmossdk.io/core/transaction" "cosmossdk.io/log" serverv2 "cosmossdk.io/server/v2" + "cosmossdk.io/server/v2/appmanager" ) var ( @@ -37,6 +38,7 @@ func New[T transaction.Tx]( logger log.Logger, config server.ConfigMap, ir jsonpb.AnyResolver, + appManager appmanager.AppManager[T], cfgOptions ...CfgOption, ) (*Server[T], error) { // The default JSON marshaller used by the gRPC-Gateway is unable to marshal non-nullable non-scalar fields. @@ -76,7 +78,7 @@ func New[T transaction.Tx]( s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - mux.Handle("/", NewTunnel[T](s.GRPCGatewayRouter)) + mux.Handle("/", NewGatewayInterceptor[T](s.GRPCGatewayRouter, appManager)) s.server = &http.Server{ Addr: s.config.Address, diff --git a/server/v2/api/grpcgateway/tunnel.go b/server/v2/api/grpcgateway/tunnel.go deleted file mode 100644 index 7d1010439a62..000000000000 --- a/server/v2/api/grpcgateway/tunnel.go +++ /dev/null @@ -1,50 +0,0 @@ -package grpcgateway - -import ( - "net/http" - - gogoproto "github.com/cosmos/gogoproto/proto" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - - "cosmossdk.io/core/transaction" - "cosmossdk.io/server/v2/appmanager" -) - -var _ http.Handler = &Tunnel[transaction.Tx]{} - -type Tunnel[T transaction.Tx] struct { - // gateway is the fallback grpc gateway mux handler. - gateway *runtime.ServeMux - - // customEndpointMapping is a mapping of custom GET options on proto RPC handlers, to the fully qualified method name. - // - // example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata - customEndpointMapping map[string]string - - appManager appmanager.AppManager[T] -} - -func NewTunnel[T transaction.Tx](gateway *runtime.ServeMux) *Tunnel[T] { - return &Tunnel[T]{gateway: gateway} -} - -// Handle some things: -// -// - see if we can match the request to -func (t *Tunnel[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - uri := request.URL.RequestURI() - uriMatch := matchURI(uri, t.customEndpointMapping) - if uriMatch != nil { - if uriMatch.HasParams() { - - } else { - - } - } else { - t.gateway.ServeHTTP(writer, request) - } -} - -func createMessage(method string) (gogoproto.Message, error) { - return nil, nil -} diff --git a/server/v2/api/grpcgateway/tunnel_test.go b/server/v2/api/grpcgateway/tunnel_test.go deleted file mode 100644 index e7095f7045ed..000000000000 --- a/server/v2/api/grpcgateway/tunnel_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package grpcgateway - -import ( - "encoding/json" - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestThing(t *testing.T) { - bz, err := os.ReadFile("mapping.json") - require.NoError(t, err) - var mapping map[string]string - err = json.Unmarshal(bz, &mapping) - require.NoError(t, err) - - match := matchURI("/cosmos/bank/v1beta1/denom_owners/ibc/denom/1", mapping) - fmt.Println(match) -} diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index faaa2b95be66..b7148225894b 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -1,14 +1,29 @@ package grpcgateway import ( + "fmt" + "io" + "net/http" + "reflect" "regexp" "strings" + + "github.com/cosmos/gogoproto/jsonpb" + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/mitchellh/mapstructure" ) +const MaxBodySize = 1 << 20 // 1 MB + // URIMatch contains the matching results type URIMatch struct { - MethodName string - Params map[string]string + // QueryInputName is the fully qualified name of the proto input type of the query. + QueryInputName string + + // Params are any wildcard params found in the request. + // + // example: foo/bar/{baz} - foo/bar/qux -> {baz: qux} + Params map[string]string } func (uri URIMatch) HasParams() bool { @@ -16,16 +31,23 @@ func (uri URIMatch) HasParams() bool { } // matchURI checks if a given URI matches any pattern and extracts wildcard values -func matchURI(uri string, patterns map[string]string) *URIMatch { +func matchURI(uri string, getPatternToQueryInputName map[string]string) *URIMatch { // Remove trailing slash if present uri = strings.TrimRight(uri, "/") - for pattern, methodName := range patterns { - // Remove trailing slash from pattern if present - pattern = strings.TrimRight(pattern, "/") + // for simple cases, where there are no wildcards, we can just do a map lookup. + if inputName, ok := getPatternToQueryInputName[uri]; ok { + return &URIMatch{ + QueryInputName: inputName, + } + } + + for getPattern, queryInputName := range getPatternToQueryInputName { + // Remove trailing slash from getPattern if present + getPattern = strings.TrimRight(getPattern, "/") - // Get regex pattern and param names - regexPattern, paramNames := patternToRegex(pattern) + // Get regex getPattern and param names + regexPattern, paramNames := patternToRegex(getPattern) // Compile and match regex := regexp.MustCompile(regexPattern) @@ -39,8 +61,8 @@ func matchURI(uri string, patterns map[string]string) *URIMatch { } return &URIMatch{ - MethodName: methodName, - Params: params, + QueryInputName: queryInputName, + Params: params, } } } @@ -74,3 +96,71 @@ func patternToRegex(pattern string) (string, []string) { return "^" + escaped + "$", paramNames } + +func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, error) { + requestType := gogoproto.MessageType(match.QueryInputName) + if requestType == nil { + return nil, fmt.Errorf("unknown request type") + } + + msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) + if !ok { + return nil, fmt.Errorf("failed to create message instance") + } + + defer r.Body.Close() + limitedReader := io.LimitReader(r.Body, MaxBodySize) + err := jsonpb.Unmarshal(limitedReader, msg) + if err != nil { + return nil, fmt.Errorf("error parsing body: %w", err) + } + + return msg, nil + +} + +func createMessage(match *URIMatch) (gogoproto.Message, error) { + requestType := gogoproto.MessageType(match.QueryInputName) + if requestType == nil { + return nil, fmt.Errorf("unknown request type") + } + + msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) + if !ok { + return nil, fmt.Errorf("failed to create message instance") + } + + if match.HasParams() { + // Create a map with the proper field names from protobuf tags + fieldMap := make(map[string]string) + v := reflect.ValueOf(msg).Elem() + t := v.Type() + + for key, value := range match.Params { + // Find the corresponding struct field + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tag := field.Tag.Get("protobuf") + if nameMatch := regexp.MustCompile(`name=(\w+)`).FindStringSubmatch(tag); len(nameMatch) > 1 { + if nameMatch[1] == key { + fieldMap[field.Name] = value // Use the actual field name + break + } + } + } + } + + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: msg, + WeaklyTypedInput: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to create decoder: %w", err) + } + + if err := decoder.Decode(fieldMap); err != nil { + return nil, fmt.Errorf("failed to decode params: %w", err) + } + } + return msg, nil +} diff --git a/simapp/v2/simdv2/cmd/commands.go b/simapp/v2/simdv2/cmd/commands.go index 2a7634ffea98..f9b9e6ebfc09 100644 --- a/simapp/v2/simdv2/cmd/commands.go +++ b/simapp/v2/simdv2/cmd/commands.go @@ -30,7 +30,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/rpc" sdktelemetry "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/version" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" @@ -157,6 +156,7 @@ func InitRootCmd[T transaction.Tx]( logger, deps.GlobalConfig, simApp.InterfaceRegistry(), + simApp.App.AppManager, ) if err != nil { return nil, err @@ -278,10 +278,4 @@ func registerGRPCGatewayRoutes[T transaction.Tx]( cmtservice.RegisterGRPCGatewayRoutes(deps.ClientContext, server.GRPCGatewayRouter) _ = nodeservice.RegisterServiceHandlerClient(context.Background(), server.GRPCGatewayRouter, nodeservice.NewServiceClient(deps.ClientContext)) _ = txtypes.RegisterServiceHandlerClient(context.Background(), server.GRPCGatewayRouter, txtypes.NewServiceClient(deps.ClientContext)) - - for _, mod := range deps.ModuleManager.Modules() { - if gmod, ok := mod.(module.HasGRPCGateway); ok { - gmod.RegisterGRPCGatewayRoutes(deps.ClientContext, server.GRPCGatewayRouter) - } - } } diff --git a/x/authz/register.go b/x/authz/register.go deleted file mode 100644 index bacc40af4f40..000000000000 --- a/x/authz/register.go +++ /dev/null @@ -1,50 +0,0 @@ -package authz - -import ( - "fmt" - - gogoproto "github.com/cosmos/gogoproto/proto" - "google.golang.org/genproto/googleapis/api/annotations" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -// GetProtoHTTPGetRuleMapping returns a mapping of proto method full name to it's HTTP GET annotation. -func GetProtoHTTPGetRuleMapping() map[string]string { - protoFiles, err := gogoproto.MergedRegistry() - if err != nil { - panic(err) - } - - httpGets := make(map[string]string) - protoFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { - for i := 0; i < fd.Services().Len(); i++ { - // Get the service descriptor - sd := fd.Services().Get(i) - - for j := 0; j < sd.Methods().Len(); j++ { - // Get the method descriptor - md := sd.Methods().Get(j) - - httpOption := proto.GetExtension(md.Options(), annotations.E_Http) - if httpOption == nil { - continue - } - - httpRule, ok := httpOption.(*annotations.HttpRule) - if !ok || httpRule == nil { - continue - } - if httpRule.GetGet() == "" { - continue - } - - httpGets[httpRule.GetGet()] = string(md.FullName()) - fmt.Printf("service: %q \t get option: %q\n", md.FullName(), httpRule.GetGet()) - } - } - return true - }) - - return httpGets -} diff --git a/x/authz/register_test.go b/x/authz/register_test.go deleted file mode 100644 index 8abd565c766d..000000000000 --- a/x/authz/register_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package authz - -import ( - "encoding/json" - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRegister(t *testing.T) { - m := GetProtoHTTPGetRuleMapping() - bz, err := json.Marshal(m) - require.NoError(t, err) - os.WriteFile("mapping.json", bz, 0600) -} From ddd164bce388dbe21784925a31897a1987a6198b Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:40:41 -0800 Subject: [PATCH 04/25] didnt mean to commit that --- x/authz/mapping.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 x/authz/mapping.json diff --git a/x/authz/mapping.json b/x/authz/mapping.json deleted file mode 100644 index 6f2edad37300..000000000000 --- a/x/authz/mapping.json +++ /dev/null @@ -1 +0,0 @@ -{"/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo","/cosmos/auth/v1beta1/accounts":"cosmos.auth.v1beta1.Query.Accounts","/cosmos/auth/v1beta1/accounts/{address}":"cosmos.auth.v1beta1.Query.Account","/cosmos/auth/v1beta1/address_by_id/{account_id}":"cosmos.auth.v1beta1.Query.AccountAddressByID","/cosmos/auth/v1beta1/bech32":"cosmos.auth.v1beta1.Query.Bech32Prefix","/cosmos/auth/v1beta1/bech32/{address_bytes}":"cosmos.auth.v1beta1.Query.AddressBytesToString","/cosmos/auth/v1beta1/bech32/{address_string}":"cosmos.auth.v1beta1.Query.AddressStringToBytes","/cosmos/auth/v1beta1/module_accounts":"cosmos.auth.v1beta1.Query.ModuleAccounts","/cosmos/auth/v1beta1/module_accounts/{name}":"cosmos.auth.v1beta1.Query.ModuleAccountByName","/cosmos/auth/v1beta1/params":"cosmos.auth.v1beta1.Query.Params","/cosmos/authz/v1beta1/grants":"cosmos.authz.v1beta1.Query.Grants","/cosmos/authz/v1beta1/grants/grantee/{grantee}":"cosmos.authz.v1beta1.Query.GranteeGrants","/cosmos/authz/v1beta1/grants/granter/{granter}":"cosmos.authz.v1beta1.Query.GranterGrants","/cosmos/bank/v1beta1/balances/{address}":"cosmos.bank.v1beta1.Query.AllBalances","/cosmos/bank/v1beta1/balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.Balance","/cosmos/bank/v1beta1/denom_owners/{denom=**}":"cosmos.bank.v1beta1.Query.DenomOwners","/cosmos/bank/v1beta1/denom_owners_by_query":"cosmos.bank.v1beta1.Query.DenomOwnersByQuery","/cosmos/bank/v1beta1/denoms_metadata":"cosmos.bank.v1beta1.Query.DenomsMetadata","/cosmos/bank/v1beta1/denoms_metadata/{denom=**}":"cosmos.bank.v1beta1.Query.DenomMetadata","/cosmos/bank/v1beta1/denoms_metadata_by_query_string":"cosmos.bank.v1beta1.Query.DenomMetadataByQueryString","/cosmos/bank/v1beta1/params":"cosmos.bank.v1beta1.Query.Params","/cosmos/bank/v1beta1/send_enabled":"cosmos.bank.v1beta1.Query.SendEnabled","/cosmos/bank/v1beta1/spendable_balances/{address}":"cosmos.bank.v1beta1.Query.SpendableBalances","/cosmos/bank/v1beta1/spendable_balances/{address}/by_denom":"cosmos.bank.v1beta1.Query.SpendableBalanceByDenom","/cosmos/bank/v1beta1/supply":"cosmos.bank.v1beta1.Query.TotalSupply","/cosmos/bank/v1beta1/supply/by_denom":"cosmos.bank.v1beta1.Query.SupplyOf","/cosmos/staking/v1beta1/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.DelegatorDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations":"cosmos.staking.v1beta1.Query.Redelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.DelegatorUnbondingDelegations","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators":"cosmos.staking.v1beta1.Query.DelegatorValidators","/cosmos/staking/v1beta1/delegators/{delegator_addr}/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.DelegatorValidator","/cosmos/staking/v1beta1/historical_info/{height}":"cosmos.staking.v1beta1.Query.HistoricalInfo","/cosmos/staking/v1beta1/params":"cosmos.staking.v1beta1.Query.Params","/cosmos/staking/v1beta1/pool":"cosmos.staking.v1beta1.Query.Pool","/cosmos/staking/v1beta1/validators":"cosmos.staking.v1beta1.Query.Validators","/cosmos/staking/v1beta1/validators/{validator_addr}":"cosmos.staking.v1beta1.Query.Validator","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations":"cosmos.staking.v1beta1.Query.ValidatorDelegations","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}":"cosmos.staking.v1beta1.Query.Delegation","/cosmos/staking/v1beta1/validators/{validator_addr}/delegations/{delegator_addr}/unbonding_delegation":"cosmos.staking.v1beta1.Query.UnbondingDelegation","/cosmos/staking/v1beta1/validators/{validator_addr}/unbonding_delegations":"cosmos.staking.v1beta1.Query.ValidatorUnbondingDelegations","/cosmos/tx/v1beta1/txs":"cosmos.tx.v1beta1.Service.GetTxsEvent","/cosmos/tx/v1beta1/txs/block/{height}":"cosmos.tx.v1beta1.Service.GetBlockWithTxs","/cosmos/tx/v1beta1/txs/{hash}":"cosmos.tx.v1beta1.Service.GetTx"} \ No newline at end of file From 64a86058923716f37dd065efe8ffe38321f43830 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:59:18 -0800 Subject: [PATCH 05/25] comments --- server/v2/api/grpcgateway/interceptor.go | 34 ++++++++------ server/v2/api/grpcgateway/server.go | 6 ++- server/v2/api/grpcgateway/uri.go | 59 +++++++++++++----------- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index f6a6318619f4..9cdad6688970 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -18,6 +18,7 @@ import ( var _ http.Handler = &GatewayInterceptor[transaction.Tx]{} +// GatewayInterceptor handles routing grpc-gateway queries to the app manager's query router. type GatewayInterceptor[T transaction.Tx] struct { // gateway is the fallback grpc gateway mux handler. gateway *runtime.ServeMux @@ -27,27 +28,30 @@ type GatewayInterceptor[T transaction.Tx] struct { // example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata customEndpointMapping map[string]string - // appManager is used to route queries through the SDK router. + // appManager is used to route queries to the application. appManager appmanager.AppManager[T] } -func NewGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) *GatewayInterceptor[T] { +// NewGatewayInterceptor creates a new GatewayInterceptor. +func NewGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*GatewayInterceptor[T], error) { + getMapping, err := getHTTPGetAnnotationMapping() + if err != nil { + return nil, err + } return &GatewayInterceptor[T]{ gateway: gateway, - customEndpointMapping: GetProtoHTTPGetRuleMapping(), + customEndpointMapping: getMapping, appManager: am, - } + }, nil } +// ServeHTTP implements the http.Handler interface. This function will attempt to match http requests to the +// interceptors internal mapping of http annotations to query request type names. +// If no match can be made, it falls back to the runtime gateway server mux. func (g *GatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - fmt.Println("printing mapping") - fmt.Println(g.customEndpointMapping) - fmt.Println("got request for: ", request.URL.Path) uri := request.URL.RequestURI() - fmt.Println("checking URI: ", uri) uriMatch := matchURI(uri, g.customEndpointMapping) if uriMatch != nil { - fmt.Println("got match: ", uriMatch.QueryInputName) var msg gogoproto.Message var err error @@ -75,16 +79,17 @@ func (g *GatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h http.Error(writer, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) } } else { - fmt.Println("no custom endpoint mapping found, falling back to gateway router") g.gateway.ServeHTTP(writer, request) } } -// GetProtoHTTPGetRuleMapping returns a mapping of proto method full name to it's HTTP GET annotation. -func GetProtoHTTPGetRuleMapping() map[string]string { +// getHTTPGetAnnotationMapping returns a mapping of proto query input type full name to its RPC method's HTTP GET annotation. +// +// example: "/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo" +func getHTTPGetAnnotationMapping() (map[string]string, error) { protoFiles, err := gogoproto.MergedRegistry() if err != nil { - panic(err) + return nil, err } httpGets := make(map[string]string) @@ -111,11 +116,10 @@ func GetProtoHTTPGetRuleMapping() map[string]string { } httpGets[httpRule.GetGet()] = string(md.Input().FullName()) - fmt.Printf("input name: %q \t get option: %q\n", md.Input().FullName(), httpRule.GetGet()) } } return true }) - return httpGets + return httpGets, nil } diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index d7fa58198082..886dc4c462fb 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -78,7 +78,11 @@ func New[T transaction.Tx]( s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - mux.Handle("/", NewGatewayInterceptor[T](s.GRPCGatewayRouter, appManager)) + interceptor, err := NewGatewayInterceptor[T](s.GRPCGatewayRouter, appManager) + if err != nil { + return nil, fmt.Errorf("failed to create grpc-gateway interceptor: %w", err) + } + mux.Handle("/", interceptor) s.server = &http.Server{ Addr: s.config.Address, diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index b7148225894b..c38bcaa43412 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -15,9 +15,9 @@ import ( const MaxBodySize = 1 << 20 // 1 MB -// URIMatch contains the matching results +// URIMatch contains information related to a URI match. type URIMatch struct { - // QueryInputName is the fully qualified name of the proto input type of the query. + // QueryInputName is the fully qualified name of the proto input type of the query rpc method. QueryInputName string // Params are any wildcard params found in the request. @@ -26,37 +26,36 @@ type URIMatch struct { Params map[string]string } +// HasParams reports whether the URIMatch has any params. func (uri URIMatch) HasParams() bool { return len(uri.Params) > 0 } -// matchURI checks if a given URI matches any pattern and extracts wildcard values +// matchURI attempts to find a match for the given URI. +// NOTE: if no match is found, nil is returned. func matchURI(uri string, getPatternToQueryInputName map[string]string) *URIMatch { - // Remove trailing slash if present uri = strings.TrimRight(uri, "/") - // for simple cases, where there are no wildcards, we can just do a map lookup. + // for simple cases where there are no wildcards, we can just do a map lookup. if inputName, ok := getPatternToQueryInputName[uri]; ok { return &URIMatch{ QueryInputName: inputName, } } + // attempt to find a match in the pattern map. for getPattern, queryInputName := range getPatternToQueryInputName { - // Remove trailing slash from getPattern if present getPattern = strings.TrimRight(getPattern, "/") - // Get regex getPattern and param names - regexPattern, paramNames := patternToRegex(getPattern) + regexPattern, wildcardNames := patternToRegex(getPattern) - // Compile and match regex := regexp.MustCompile(regexPattern) matches := regex.FindStringSubmatch(uri) if matches != nil && len(matches) > 1 { - // First match is the full string, subsequent matches are capture groups + // first match is the full string, subsequent matches are capture groups params := make(map[string]string) - for i, name := range paramNames { + for i, name := range wildcardNames { params[name] = matches[i+1] } @@ -70,33 +69,34 @@ func matchURI(uri string, getPatternToQueryInputName map[string]string) *URIMatc return nil } -// patternToRegex converts a URI pattern with wildcards to a regex pattern -// Returns the regex pattern and a slice of parameter names in order +// patternToRegex converts a URI pattern with wildcards to a regex pattern. +// Returns the regex pattern and a slice of wildcard names in order func patternToRegex(pattern string) (string, []string) { escaped := regexp.QuoteMeta(pattern) - var paramNames []string + var wildcardNames []string - // Extract and replace {param=**} patterns - r1 := regexp.MustCompile(`\\\{([^}]+?)=\\\*\\\*\\\}`) + // extract and replace {param=**} patterns + r1 := regexp.MustCompile(`\\\{([^}]+?)=\\\*\\\*\\}`) escaped = r1.ReplaceAllStringFunc(escaped, func(match string) string { - // Extract param name without the =** suffix + // extract wildcard name without the =** suffix name := regexp.MustCompile(`\\\{(.+?)=`).FindStringSubmatch(match)[1] - paramNames = append(paramNames, name) + wildcardNames = append(wildcardNames, name) return "(.+)" }) - // Extract and replace {param} patterns - r2 := regexp.MustCompile(`\\\{([^}]+)\\\}`) + // extract and replace {param} patterns + r2 := regexp.MustCompile(`\\\{([^}]+)\\}`) escaped = r2.ReplaceAllStringFunc(escaped, func(match string) string { - // Extract param name from between { and } - name := regexp.MustCompile(`\\\{(.*?)\\\}`).FindStringSubmatch(match)[1] - paramNames = append(paramNames, name) + // extract wildcard name from the curl braces {}. + name := regexp.MustCompile(`\\\{(.*?)\\}`).FindStringSubmatch(match)[1] + wildcardNames = append(wildcardNames, name) return "([^/]+)" }) - return "^" + escaped + "$", paramNames + return "^" + escaped + "$", wildcardNames } +// createMessageFromJSON creates a message from the URIMatch given the JSON body in the http request. func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { @@ -119,6 +119,8 @@ func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, } +// createMessage creates a message from the given URIMatch. If the match has params, the message will be populated +// with the value of those params. Otherwise, an empty message is returned. func createMessage(match *URIMatch) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { @@ -130,20 +132,21 @@ func createMessage(match *URIMatch) (gogoproto.Message, error) { return nil, fmt.Errorf("failed to create message instance") } + // if the uri match has params, we need to populate the message with the values of those params. if match.HasParams() { - // Create a map with the proper field names from protobuf tags + // create a map with the proper field names from protobuf tags fieldMap := make(map[string]string) v := reflect.ValueOf(msg).Elem() t := v.Type() for key, value := range match.Params { - // Find the corresponding struct field + // attempt to match wildcard name to protobuf struct tag. for i := 0; i < t.NumField(); i++ { field := t.Field(i) tag := field.Tag.Get("protobuf") if nameMatch := regexp.MustCompile(`name=(\w+)`).FindStringSubmatch(tag); len(nameMatch) > 1 { if nameMatch[1] == key { - fieldMap[field.Name] = value // Use the actual field name + fieldMap[field.Name] = value break } } @@ -152,7 +155,7 @@ func createMessage(match *URIMatch) (gogoproto.Message, error) { decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: msg, - WeaklyTypedInput: true, + WeaklyTypedInput: true, // TODO(technicallyty): should we put false here? }) if err != nil { return nil, fmt.Errorf("failed to create decoder: %w", err) From 5b8a773d05ef9fbdae440af08bafd5b7c3bcada8 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:16:41 -0800 Subject: [PATCH 06/25] unexport things that don't need to be exported --- server/v2/api/grpcgateway/interceptor.go | 14 ++++++------- server/v2/api/grpcgateway/server.go | 2 +- server/v2/api/grpcgateway/uri.go | 26 ++++++++++++------------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 9cdad6688970..5fd44f1439a5 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -16,10 +16,10 @@ import ( "cosmossdk.io/server/v2/appmanager" ) -var _ http.Handler = &GatewayInterceptor[transaction.Tx]{} +var _ http.Handler = &gatewayInterceptor[transaction.Tx]{} -// GatewayInterceptor handles routing grpc-gateway queries to the app manager's query router. -type GatewayInterceptor[T transaction.Tx] struct { +// gatewayInterceptor handles routing grpc-gateway queries to the app manager's query router. +type gatewayInterceptor[T transaction.Tx] struct { // gateway is the fallback grpc gateway mux handler. gateway *runtime.ServeMux @@ -32,13 +32,13 @@ type GatewayInterceptor[T transaction.Tx] struct { appManager appmanager.AppManager[T] } -// NewGatewayInterceptor creates a new GatewayInterceptor. -func NewGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*GatewayInterceptor[T], error) { +// newGatewayInterceptor creates a new gatewayInterceptor. +func newGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*gatewayInterceptor[T], error) { getMapping, err := getHTTPGetAnnotationMapping() if err != nil { return nil, err } - return &GatewayInterceptor[T]{ + return &gatewayInterceptor[T]{ gateway: gateway, customEndpointMapping: getMapping, appManager: am, @@ -48,7 +48,7 @@ func NewGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appma // ServeHTTP implements the http.Handler interface. This function will attempt to match http requests to the // interceptors internal mapping of http annotations to query request type names. // If no match can be made, it falls back to the runtime gateway server mux. -func (g *GatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { +func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { uri := request.URL.RequestURI() uriMatch := matchURI(uri, g.customEndpointMapping) if uriMatch != nil { diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 886dc4c462fb..31a67e6f0e4c 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -78,7 +78,7 @@ func New[T transaction.Tx]( s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - interceptor, err := NewGatewayInterceptor[T](s.GRPCGatewayRouter, appManager) + interceptor, err := newGatewayInterceptor[T](s.GRPCGatewayRouter, appManager) if err != nil { return nil, fmt.Errorf("failed to create grpc-gateway interceptor: %w", err) } diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index c38bcaa43412..bd12a61897c6 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -13,10 +13,10 @@ import ( "github.com/mitchellh/mapstructure" ) -const MaxBodySize = 1 << 20 // 1 MB +const maxBodySize = 1 << 20 // 1 MB -// URIMatch contains information related to a URI match. -type URIMatch struct { +// uriMatch contains information related to a URI match. +type uriMatch struct { // QueryInputName is the fully qualified name of the proto input type of the query rpc method. QueryInputName string @@ -26,19 +26,19 @@ type URIMatch struct { Params map[string]string } -// HasParams reports whether the URIMatch has any params. -func (uri URIMatch) HasParams() bool { +// HasParams reports whether the uriMatch has any params. +func (uri uriMatch) HasParams() bool { return len(uri.Params) > 0 } // matchURI attempts to find a match for the given URI. // NOTE: if no match is found, nil is returned. -func matchURI(uri string, getPatternToQueryInputName map[string]string) *URIMatch { +func matchURI(uri string, getPatternToQueryInputName map[string]string) *uriMatch { uri = strings.TrimRight(uri, "/") // for simple cases where there are no wildcards, we can just do a map lookup. if inputName, ok := getPatternToQueryInputName[uri]; ok { - return &URIMatch{ + return &uriMatch{ QueryInputName: inputName, } } @@ -59,7 +59,7 @@ func matchURI(uri string, getPatternToQueryInputName map[string]string) *URIMatc params[name] = matches[i+1] } - return &URIMatch{ + return &uriMatch{ QueryInputName: queryInputName, Params: params, } @@ -96,8 +96,8 @@ func patternToRegex(pattern string) (string, []string) { return "^" + escaped + "$", wildcardNames } -// createMessageFromJSON creates a message from the URIMatch given the JSON body in the http request. -func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, error) { +// createMessageFromJSON creates a message from the uriMatch given the JSON body in the http request. +func createMessageFromJSON(match *uriMatch, r *http.Request) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { return nil, fmt.Errorf("unknown request type") @@ -109,7 +109,7 @@ func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, } defer r.Body.Close() - limitedReader := io.LimitReader(r.Body, MaxBodySize) + limitedReader := io.LimitReader(r.Body, maxBodySize) err := jsonpb.Unmarshal(limitedReader, msg) if err != nil { return nil, fmt.Errorf("error parsing body: %w", err) @@ -119,9 +119,9 @@ func createMessageFromJSON(match *URIMatch, r *http.Request) (gogoproto.Message, } -// createMessage creates a message from the given URIMatch. If the match has params, the message will be populated +// createMessage creates a message from the given uriMatch. If the match has params, the message will be populated // with the value of those params. Otherwise, an empty message is returned. -func createMessage(match *URIMatch) (gogoproto.Message, error) { +func createMessage(match *uriMatch) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { return nil, fmt.Errorf("unknown request type") From 14adff2e6d6fedf30a7ffb7dc2fae253a609c309 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:45:46 -0800 Subject: [PATCH 07/25] add some unit tests --- server/v2/api/grpcgateway/uri_test.go | 200 ++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 server/v2/api/grpcgateway/uri_test.go diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go new file mode 100644 index 000000000000..b4d922a53ec7 --- /dev/null +++ b/server/v2/api/grpcgateway/uri_test.go @@ -0,0 +1,200 @@ +package grpcgateway + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "testing" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" +) + +func TestMatchURI(t *testing.T) { + testCases := []struct { + name string + uri string + mapping map[string]string + expected *uriMatch + }{ + { + name: "simple match, no wildcards", + uri: "/foo/bar", + mapping: map[string]string{"/foo/bar": "bar"}, + expected: &uriMatch{QueryInputName: "bar"}, + }, + { + name: "wildcard match at the end", + uri: "/foo/bar/buzz", + mapping: map[string]string{"/foo/bar/{baz}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"baz": "buzz"}, + }, + }, + { + name: "wildcard match in the middle", + uri: "/foo/buzz/bar", + mapping: map[string]string{"/foo/{baz}/bar": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"baz": "buzz"}, + }, + }, + { + name: "multiple wild cards", + uri: "/foo/bar/baz/buzz", + mapping: map[string]string{"/foo/bar/{q1}/{q2}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"q1": "baz", "q2": "buzz"}, + }, + }, + { + name: "catch-all wildcard", + uri: "/foo/bar/ibc/token/stuff", + mapping: map[string]string{"/foo/bar/{ibc_token=**}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"ibc_token": "ibc/token/stuff"}, + }, + }, + { + name: "no match should return nil", + uri: "/foo/bar", + mapping: map[string]string{"/bar/foo": "bar"}, + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := matchURI(tc.uri, tc.mapping) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestURIMatch_HasParams(t *testing.T) { + u := uriMatch{Params: map[string]string{"foo": "bar"}} + require.True(t, u.HasParams()) + + u = uriMatch{} + require.False(t, u.HasParams()) +} + +const dummyProtoName = "dummy" + +type DummyProto struct { + Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` + Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` + Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` +} + +func (d DummyProto) Reset() {} + +func (d DummyProto) String() string { return dummyProtoName } + +func (d DummyProto) ProtoMessage() {} + +func TestCreateMessage(t *testing.T) { + gogoproto.RegisterType(&DummyProto{}, dummyProtoName) + + testCases := []struct { + name string + uri uriMatch + expected gogoproto.Message + expErr bool + }{ + { + name: "simple, empty message", + uri: uriMatch{QueryInputName: dummyProtoName}, + expected: &DummyProto{}, + }, + { + name: "message with params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + }, + }, + { + name: "invalid params should error out", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "235235", "baz": "true"}, + }, + expErr: true, + }, + { + name: "unknown input type", + uri: uriMatch{ + QueryInputName: "foobar", + }, + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := createMessage(&tc.uri) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } + }) + } +} + +func TestCreateMessageFromJson(t *testing.T) { + gogoproto.RegisterType(&DummyProto{}, dummyProtoName) + testCases := []struct { + name string + uri uriMatch + request func() *http.Request + expected gogoproto.Message + }{ + { + name: "simple, empty message", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + return &http.Request{Body: io.NopCloser(bytes.NewReader([]byte("{}")))} + }, + expected: &DummyProto{}, + }, + { + name: "message with json input", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + d := DummyProto{ + Foo: "hello", + Bar: true, + Baz: 320, + } + bz, err := json.Marshal(d) + require.NoError(t, err) + return &http.Request{Body: io.NopCloser(bytes.NewReader(bz))} + }, + expected: &DummyProto{ + Foo: "hello", + Bar: true, + Baz: 320, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := createMessageFromJSON(&tc.uri, tc.request()) + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + }) + } +} From bb2f221c2b15f1df9dfe7fb393f10da0bc811742 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:45:03 -0800 Subject: [PATCH 08/25] make uri matcher work with query params --- server/v2/api/grpcgateway/interceptor.go | 9 +++---- server/v2/api/grpcgateway/uri.go | 24 ++++++++++++----- server/v2/api/grpcgateway/uri_test.go | 33 +++++++++++++++++------- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 5fd44f1439a5..e761b4b4ccca 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -49,17 +49,16 @@ func newGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appma // interceptors internal mapping of http annotations to query request type names. // If no match can be made, it falls back to the runtime gateway server mux. func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - uri := request.URL.RequestURI() - uriMatch := matchURI(uri, g.customEndpointMapping) - if uriMatch != nil { + match := matchURL(request.URL, g.customEndpointMapping) + if match != nil { var msg gogoproto.Message var err error switch request.Method { case http.MethodPost: - msg, err = createMessageFromJSON(uriMatch, request) + msg, err = createMessageFromJSON(match, request) case http.MethodGet: - msg, err = createMessage(uriMatch) + msg, err = createMessage(match) default: http.Error(writer, "unsupported http method", http.StatusMethodNotAllowed) return diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index bd12a61897c6..a90ae81a9a63 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "net/url" "reflect" "regexp" "strings" @@ -31,15 +32,27 @@ func (uri uriMatch) HasParams() bool { return len(uri.Params) > 0 } -// matchURI attempts to find a match for the given URI. +// matchURL attempts to find a match for the given URL. // NOTE: if no match is found, nil is returned. -func matchURI(uri string, getPatternToQueryInputName map[string]string) *uriMatch { - uri = strings.TrimRight(uri, "/") +func matchURL(u *url.URL, getPatternToQueryInputName map[string]string) *uriMatch { + uriPath := strings.TrimRight(u.Path, "/") + queryParams := u.Query() + + params := make(map[string]string) + for key, vals := range queryParams { + if len(vals) > 0 { + // url.Values contains a slice for the values as you are able to specify a key multiple times in URL. + // example: https://localhost:9090/do/something?color=red&color=blue&color=green + // We will just take the first value in the slice. + params[key] = vals[0] + } + } // for simple cases where there are no wildcards, we can just do a map lookup. - if inputName, ok := getPatternToQueryInputName[uri]; ok { + if inputName, ok := getPatternToQueryInputName[uriPath]; ok { return &uriMatch{ QueryInputName: inputName, + Params: params, } } @@ -50,11 +63,10 @@ func matchURI(uri string, getPatternToQueryInputName map[string]string) *uriMatc regexPattern, wildcardNames := patternToRegex(getPattern) regex := regexp.MustCompile(regexPattern) - matches := regex.FindStringSubmatch(uri) + matches := regex.FindStringSubmatch(uriPath) if matches != nil && len(matches) > 1 { // first match is the full string, subsequent matches are capture groups - params := make(map[string]string) for i, name := range wildcardNames { params[name] = matches[i+1] } diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go index b4d922a53ec7..e1afd84c9c19 100644 --- a/server/v2/api/grpcgateway/uri_test.go +++ b/server/v2/api/grpcgateway/uri_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "net/http" + "net/url" "testing" gogoproto "github.com/cosmos/gogoproto/proto" @@ -20,13 +21,25 @@ func TestMatchURI(t *testing.T) { }{ { name: "simple match, no wildcards", - uri: "/foo/bar", - mapping: map[string]string{"/foo/bar": "bar"}, - expected: &uriMatch{QueryInputName: "bar"}, + uri: "https://localhost:8080/foo/bar", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{}}, + }, + { + name: "match with query parameters", + uri: "https://localhost:8080/foo/bar?baz=qux", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{"baz": "qux"}}, + }, + { + name: "match with multiple query parameters", + uri: "https://localhost:8080/foo/bar?baz=qux&foo=/msg.type.bank.send", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{"baz": "qux", "foo": "/msg.type.bank.send"}}, }, { name: "wildcard match at the end", - uri: "/foo/bar/buzz", + uri: "https://localhost:8080/foo/bar/buzz", mapping: map[string]string{"/foo/bar/{baz}": "bar"}, expected: &uriMatch{ QueryInputName: "bar", @@ -35,7 +48,7 @@ func TestMatchURI(t *testing.T) { }, { name: "wildcard match in the middle", - uri: "/foo/buzz/bar", + uri: "https://localhost:8080/foo/buzz/bar", mapping: map[string]string{"/foo/{baz}/bar": "bar"}, expected: &uriMatch{ QueryInputName: "bar", @@ -44,7 +57,7 @@ func TestMatchURI(t *testing.T) { }, { name: "multiple wild cards", - uri: "/foo/bar/baz/buzz", + uri: "https://localhost:8080/foo/bar/baz/buzz", mapping: map[string]string{"/foo/bar/{q1}/{q2}": "bar"}, expected: &uriMatch{ QueryInputName: "bar", @@ -53,7 +66,7 @@ func TestMatchURI(t *testing.T) { }, { name: "catch-all wildcard", - uri: "/foo/bar/ibc/token/stuff", + uri: "https://localhost:8080/foo/bar/ibc/token/stuff", mapping: map[string]string{"/foo/bar/{ibc_token=**}": "bar"}, expected: &uriMatch{ QueryInputName: "bar", @@ -62,7 +75,7 @@ func TestMatchURI(t *testing.T) { }, { name: "no match should return nil", - uri: "/foo/bar", + uri: "https://localhost:8080/foo/bar", mapping: map[string]string{"/bar/foo": "bar"}, expected: nil, }, @@ -70,7 +83,9 @@ func TestMatchURI(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actual := matchURI(tc.uri, tc.mapping) + u, err := url.Parse(tc.uri) + require.NoError(t, err) + actual := matchURL(u, tc.mapping) require.Equal(t, tc.expected, actual) }) } From e2087d87533c7dc85edf0b9dcd118813011a26e2 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:24:57 -0800 Subject: [PATCH 09/25] support for setting nested struct fields with query params --- server/v2/api/grpcgateway/interceptor.go | 28 ++++++++++++------- server/v2/api/grpcgateway/server.go | 6 ++--- server/v2/api/grpcgateway/uri.go | 34 +++++++++++++----------- server/v2/api/grpcgateway/uri_test.go | 24 ++++++++++++++--- 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index e761b4b4ccca..84f6fccab360 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -1,9 +1,9 @@ package grpcgateway import ( - "encoding/json" - "fmt" + "errors" "net/http" + "strconv" gogoproto "github.com/cosmos/gogoproto/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -51,6 +51,7 @@ func newGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appma func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { match := matchURL(request.URL, g.customEndpointMapping) if match != nil { + _, out := runtime.MarshalerForRequest(g.gateway, request) var msg gogoproto.Message var err error @@ -60,23 +61,30 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h case http.MethodGet: msg, err = createMessage(match) default: - http.Error(writer, "unsupported http method", http.StatusMethodNotAllowed) + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, errors.New(http.StatusText(http.StatusMethodNotAllowed))) return } if err != nil { - http.Error(writer, err.Error(), http.StatusBadRequest) + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) return } - query, err := g.appManager.Query(request.Context(), 0, msg) + var height uint64 + heightStr := request.Header.Get(GRPCBlockHeightHeader) + if heightStr != "" { + height, err = strconv.ParseUint(heightStr, 10, 64) + if err != nil { + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return + } + } + + query, err := g.appManager.Query(request.Context(), height, msg) if err != nil { - http.Error(writer, "Error querying", http.StatusInternalServerError) + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) return } - writer.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(writer).Encode(query); err != nil { - http.Error(writer, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) - } + runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) } else { g.gateway.ServeHTTP(writer, request) } diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 31a67e6f0e4c..5d59fc1591d6 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -139,15 +139,15 @@ func (s *Server[T]) Stop(ctx context.Context) error { return s.server.Shutdown(ctx) } +// GRPCBlockHeightHeader is the gRPC header for block height. +const GRPCBlockHeightHeader = "x-cosmos-block-height" + // CustomGRPCHeaderMatcher for mapping request headers to // GRPC metadata. // HTTP headers that start with 'Grpc-Metadata-' are automatically mapped to // gRPC metadata after removing prefix 'Grpc-Metadata-'. We can use this // CustomGRPCHeaderMatcher if headers don't start with `Grpc-Metadata-` func CustomGRPCHeaderMatcher(key string) (string, bool) { - // GRPCBlockHeightHeader is the gRPC header for block height. - const GRPCBlockHeightHeader = "x-cosmos-block-height" - switch strings.ToLower(key) { case GRPCBlockHeightHeader: return GRPCBlockHeightHeader, true diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index a90ae81a9a63..8a6f76d6f736 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -146,34 +146,38 @@ func createMessage(match *uriMatch) (gogoproto.Message, error) { // if the uri match has params, we need to populate the message with the values of those params. if match.HasParams() { - // create a map with the proper field names from protobuf tags - fieldMap := make(map[string]string) - v := reflect.ValueOf(msg).Elem() - t := v.Type() - + // convert flat params map to nested structure + nestedParams := make(map[string]any) for key, value := range match.Params { - // attempt to match wildcard name to protobuf struct tag. - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - tag := field.Tag.Get("protobuf") - if nameMatch := regexp.MustCompile(`name=(\w+)`).FindStringSubmatch(tag); len(nameMatch) > 1 { - if nameMatch[1] == key { - fieldMap[field.Name] = value - break + parts := strings.Split(key, ".") + current := nestedParams + + // step through nested levels + for i, part := range parts { + if i == len(parts)-1 { + // Last part - set the value + current[part] = value + } else { + // continue nestedness + if _, exists := current[part]; !exists { + current[part] = make(map[string]any) } + current = current[part].(map[string]any) } } } + // Configure decoder to handle the nested structure decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: msg, - WeaklyTypedInput: true, // TODO(technicallyty): should we put false here? + TagName: "json", // Use json tags as they're simpler + WeaklyTypedInput: true, }) if err != nil { return nil, fmt.Errorf("failed to create decoder: %w", err) } - if err := decoder.Decode(fieldMap); err != nil { + if err := decoder.Decode(nestedParams); err != nil { return nil, fmt.Errorf("failed to decode params: %w", err) } } diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go index e1afd84c9c19..bb74b8788453 100644 --- a/server/v2/api/grpcgateway/uri_test.go +++ b/server/v2/api/grpcgateway/uri_test.go @@ -99,12 +99,17 @@ func TestURIMatch_HasParams(t *testing.T) { require.False(t, u.HasParams()) } +type Pagination struct { + Limit int `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` +} + const dummyProtoName = "dummy" type DummyProto struct { - Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` - Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` - Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` + Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` + Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` + Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` + Page Pagination `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` } func (d DummyProto) Reset() {} @@ -139,6 +144,19 @@ func TestCreateMessage(t *testing.T) { Baz: 1352, }, }, + { + name: "message with nested params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352", "page.limit": "3"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + Page: Pagination{Limit: 3}, + }, + }, { name: "invalid params should error out", uri: uriMatch{ From c32cf9720c6a66539d12ae72d5da8cf2433b06eb Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:35:40 -0800 Subject: [PATCH 10/25] use status error codes --- server/v2/api/grpcgateway/interceptor.go | 21 +++++++++++++++++---- server/v2/api/grpcgateway/server.go | 2 +- server/v2/api/grpcgateway/uri.go | 17 +++++++++-------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 84f6fccab360..4ee027ede12a 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -2,24 +2,30 @@ package grpcgateway import ( "errors" + "fmt" "net/http" "strconv" gogoproto "github.com/cosmos/gogoproto/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/proto" "cosmossdk.io/core/transaction" + "cosmossdk.io/log" "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/stf" ) var _ http.Handler = &gatewayInterceptor[transaction.Tx]{} // gatewayInterceptor handles routing grpc-gateway queries to the app manager's query router. type gatewayInterceptor[T transaction.Tx] struct { + logger log.Logger // gateway is the fallback grpc gateway mux handler. gateway *runtime.ServeMux @@ -33,12 +39,13 @@ type gatewayInterceptor[T transaction.Tx] struct { } // newGatewayInterceptor creates a new gatewayInterceptor. -func newGatewayInterceptor[T transaction.Tx](gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*gatewayInterceptor[T], error) { +func newGatewayInterceptor[T transaction.Tx](logger log.Logger, gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*gatewayInterceptor[T], error) { getMapping, err := getHTTPGetAnnotationMapping() if err != nil { return nil, err } return &gatewayInterceptor[T]{ + logger: logger, gateway: gateway, customEndpointMapping: getMapping, appManager: am, @@ -61,7 +68,7 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h case http.MethodGet: msg, err = createMessage(match) default: - runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, errors.New(http.StatusText(http.StatusMethodNotAllowed))) + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, status.Error(codes.Unimplemented, "HTTP method must be POST or GET")) return } if err != nil { @@ -74,6 +81,7 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h if heightStr != "" { height, err = strconv.ParseUint(heightStr, 10, 64) if err != nil { + err = status.Errorf(codes.InvalidArgument, "invalid height: %s", heightStr) runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) return } @@ -81,8 +89,13 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h query, err := g.appManager.Query(request.Context(), height, msg) if err != nil { - runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) - return + if errors.Is(err, stf.ErrNoHandler) { + g.gateway.ServeHTTP(writer, request) + return + } else { + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return + } } runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) } else { diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 5d59fc1591d6..343c28fb11c9 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -78,7 +78,7 @@ func New[T transaction.Tx]( s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - interceptor, err := newGatewayInterceptor[T](s.GRPCGatewayRouter, appManager) + interceptor, err := newGatewayInterceptor[T](logger, s.GRPCGatewayRouter, appManager) if err != nil { return nil, fmt.Errorf("failed to create grpc-gateway interceptor: %w", err) } diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index 8a6f76d6f736..5ce024bd62eb 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -1,7 +1,6 @@ package grpcgateway import ( - "fmt" "io" "net/http" "net/url" @@ -12,6 +11,8 @@ import ( "github.com/cosmos/gogoproto/jsonpb" gogoproto "github.com/cosmos/gogoproto/proto" "github.com/mitchellh/mapstructure" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) const maxBodySize = 1 << 20 // 1 MB @@ -112,19 +113,19 @@ func patternToRegex(pattern string) (string, []string) { func createMessageFromJSON(match *uriMatch, r *http.Request) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { - return nil, fmt.Errorf("unknown request type") + return nil, status.Error(codes.InvalidArgument, "invalid request type") } msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) if !ok { - return nil, fmt.Errorf("failed to create message instance") + return nil, status.Error(codes.Internal, "failed to cast to proto message") } defer r.Body.Close() limitedReader := io.LimitReader(r.Body, maxBodySize) err := jsonpb.Unmarshal(limitedReader, msg) if err != nil { - return nil, fmt.Errorf("error parsing body: %w", err) + return nil, status.Error(codes.InvalidArgument, err.Error()) } return msg, nil @@ -136,12 +137,12 @@ func createMessageFromJSON(match *uriMatch, r *http.Request) (gogoproto.Message, func createMessage(match *uriMatch) (gogoproto.Message, error) { requestType := gogoproto.MessageType(match.QueryInputName) if requestType == nil { - return nil, fmt.Errorf("unknown request type") + return nil, status.Error(codes.InvalidArgument, "unknown request type") } msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) if !ok { - return nil, fmt.Errorf("failed to create message instance") + return nil, status.Error(codes.Internal, "failed to create message instance") } // if the uri match has params, we need to populate the message with the values of those params. @@ -174,11 +175,11 @@ func createMessage(match *uriMatch) (gogoproto.Message, error) { WeaklyTypedInput: true, }) if err != nil { - return nil, fmt.Errorf("failed to create decoder: %w", err) + return nil, status.Error(codes.Internal, "failed to create message instance") } if err := decoder.Decode(nestedParams); err != nil { - return nil, fmt.Errorf("failed to decode params: %w", err) + return nil, status.Error(codes.InvalidArgument, err.Error()) } } return msg, nil From fddbb50f31c2e2b3ec1259e9d7ef3147c0134666 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:36:21 -0800 Subject: [PATCH 11/25] update distribution test to match error output --- tests/systemtests/distribution_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/systemtests/distribution_test.go b/tests/systemtests/distribution_test.go index 495fd5807e81..0cc381786d32 100644 --- a/tests/systemtests/distribution_test.go +++ b/tests/systemtests/distribution_test.go @@ -179,20 +179,21 @@ func TestDistrValidatorGRPCQueries(t *testing.T) { // test validator slashes grpc endpoint slashURL := baseurl + `/cosmos/distribution/v1beta1/validators/%s/slashes` - invalidHeightOutput := `{"code":3, "message":"strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` + invalidStartingHeightOutput := `{"code":3, "message":"1 error(s) decoding:\n\n* cannot parse 'starting_height' as uint: strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` + invalidEndingHeightOutput := `{"code":3, "message":"1 error(s) decoding:\n\n* cannot parse 'ending_height' as uint: strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` slashTestCases := []systest.RestTestCase{ { Name: "invalid start height", Url: fmt.Sprintf(slashURL+`?starting_height=%s&ending_height=%s`, valOperAddr, "-3", "3"), ExpCode: http.StatusBadRequest, - ExpOut: invalidHeightOutput, + ExpOut: invalidStartingHeightOutput, }, { Name: "invalid end height", Url: fmt.Sprintf(slashURL+`?starting_height=%s&ending_height=%s`, valOperAddr, "1", "-3"), ExpCode: http.StatusBadRequest, - ExpOut: invalidHeightOutput, + ExpOut: invalidEndingHeightOutput, }, { Name: "valid request get slashes", From e8c93c6cf95ab5f29c0519737c19a7f8d6878d89 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:39:21 -0800 Subject: [PATCH 12/25] unused import --- server/v2/api/grpcgateway/interceptor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 4ee027ede12a..728d2c5ab450 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -2,7 +2,6 @@ package grpcgateway import ( "errors" - "fmt" "net/http" "strconv" From a1bee2e848982026075b36284d23876f15b54501 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:32:04 -0800 Subject: [PATCH 13/25] add some debug lines --- server/v2/api/grpcgateway/interceptor.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 728d2c5ab450..f84a8625610f 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -55,8 +55,10 @@ func newGatewayInterceptor[T transaction.Tx](logger log.Logger, gateway *runtime // interceptors internal mapping of http annotations to query request type names. // If no match can be made, it falls back to the runtime gateway server mux. func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + g.logger.Debug("received grpc-gateway request", "request_uri", request.RequestURI) match := matchURL(request.URL, g.customEndpointMapping) if match != nil { + g.logger.Debug("matched request", "query_input", match.QueryInputName) _, out := runtime.MarshalerForRequest(g.gateway, request) var msg gogoproto.Message var err error From d3bd50946128e3ab8f4be310cedac20fdcf7a247 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:35:58 -0800 Subject: [PATCH 14/25] add returns to remove else cases --- server/v2/api/grpcgateway/interceptor.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index f84a8625610f..9c8ed684fff4 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -93,15 +93,14 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h if errors.Is(err, stf.ErrNoHandler) { g.gateway.ServeHTTP(writer, request) return - } else { - runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) - return } + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return } runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) - } else { - g.gateway.ServeHTTP(writer, request) + return } + g.gateway.ServeHTTP(writer, request) } // getHTTPGetAnnotationMapping returns a mapping of proto query input type full name to its RPC method's HTTP GET annotation. From dadd86df8377b8a9caae524a0e632b5a73d62501 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:41:06 -0800 Subject: [PATCH 15/25] comments --- server/v2/api/grpcgateway/interceptor.go | 94 ++++++++++++------------ 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index 9c8ed684fff4..d604f7b92319 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -57,53 +57,58 @@ func newGatewayInterceptor[T transaction.Tx](logger log.Logger, gateway *runtime func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { g.logger.Debug("received grpc-gateway request", "request_uri", request.RequestURI) match := matchURL(request.URL, g.customEndpointMapping) - if match != nil { - g.logger.Debug("matched request", "query_input", match.QueryInputName) - _, out := runtime.MarshalerForRequest(g.gateway, request) - var msg gogoproto.Message - var err error - - switch request.Method { - case http.MethodPost: - msg, err = createMessageFromJSON(match, request) - case http.MethodGet: - msg, err = createMessage(match) - default: - runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, status.Error(codes.Unimplemented, "HTTP method must be POST or GET")) - return - } + if match == nil { + // no match cases fall back to gateway mux. + g.gateway.ServeHTTP(writer, request) + return + } + g.logger.Debug("matched request", "query_input", match.QueryInputName) + _, out := runtime.MarshalerForRequest(g.gateway, request) + var msg gogoproto.Message + var err error + + switch request.Method { + case http.MethodPost: + msg, err = createMessageFromJSON(match, request) + case http.MethodGet: + msg, err = createMessage(match) + default: + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, status.Error(codes.Unimplemented, "HTTP method must be POST or GET")) + return + } + if err != nil { + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return + } + + // extract block height header + var height uint64 + heightStr := request.Header.Get(GRPCBlockHeightHeader) + if heightStr != "" { + height, err = strconv.ParseUint(heightStr, 10, 64) if err != nil { + err = status.Errorf(codes.InvalidArgument, "invalid height: %s", heightStr) runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) return } + } - var height uint64 - heightStr := request.Header.Get(GRPCBlockHeightHeader) - if heightStr != "" { - height, err = strconv.ParseUint(heightStr, 10, 64) - if err != nil { - err = status.Errorf(codes.InvalidArgument, "invalid height: %s", heightStr) - runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) - return - } - } - - query, err := g.appManager.Query(request.Context(), height, msg) - if err != nil { - if errors.Is(err, stf.ErrNoHandler) { - g.gateway.ServeHTTP(writer, request) - return - } + query, err := g.appManager.Query(request.Context(), height, msg) + if err != nil { + // if we couldn't find a handler for this request, just fall back to the gateway mux. + if errors.Is(err, stf.ErrNoHandler) { + g.gateway.ServeHTTP(writer, request) + } else { + // for all other errors, we just return the error. runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) - return } - runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) return } - g.gateway.ServeHTTP(writer, request) + // for no errors, we forward the response. + runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) } -// getHTTPGetAnnotationMapping returns a mapping of proto query input type full name to its RPC method's HTTP GET annotation. +// getHTTPGetAnnotationMapping returns a mapping of RPC Method HTTP GET annotation to the RPC Handler's Request Input type full name. // // example: "/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo" func getHTTPGetAnnotationMapping() (map[string]string, error) { @@ -115,19 +120,16 @@ func getHTTPGetAnnotationMapping() (map[string]string, error) { httpGets := make(map[string]string) protoFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { for i := 0; i < fd.Services().Len(); i++ { - // Get the service descriptor - sd := fd.Services().Get(i) - - for j := 0; j < sd.Methods().Len(); j++ { - // Get the method descriptor - md := sd.Methods().Get(j) + serviceDesc := fd.Services().Get(i) + for j := 0; j < serviceDesc.Methods().Len(); j++ { + methodDesc := serviceDesc.Methods().Get(j) - httpOption := proto.GetExtension(md.Options(), annotations.E_Http) - if httpOption == nil { + httpAnnotation := proto.GetExtension(methodDesc.Options(), annotations.E_Http) + if httpAnnotation == nil { continue } - httpRule, ok := httpOption.(*annotations.HttpRule) + httpRule, ok := httpAnnotation.(*annotations.HttpRule) if !ok || httpRule == nil { continue } @@ -135,7 +137,7 @@ func getHTTPGetAnnotationMapping() (map[string]string, error) { continue } - httpGets[httpRule.GetGet()] = string(md.Input().FullName()) + httpGets[httpRule.GetGet()] = string(methodDesc.Input().FullName()) } } return true From 2e9285ecc2d7c5d24d9ed69ac3a1f0695947e712 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:46:06 -0800 Subject: [PATCH 16/25] add another test case --- server/v2/api/grpcgateway/uri_test.go | 30 +++++++++++++++++++++------ server/v2/go.mod | 3 ++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go index bb74b8788453..063058505667 100644 --- a/server/v2/api/grpcgateway/uri_test.go +++ b/server/v2/api/grpcgateway/uri_test.go @@ -99,17 +99,22 @@ func TestURIMatch_HasParams(t *testing.T) { require.False(t, u.HasParams()) } +type Nested struct { + Foo int `protobuf:"varint,1,opt,name=foo,proto3" json:"foo,omitempty"` +} + type Pagination struct { - Limit int `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Limit int `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Nest *Nested `protobuf:"bytes,2,opt,name=nest,proto3" json:"nest,omitempty"` } const dummyProtoName = "dummy" type DummyProto struct { - Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` - Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` - Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` - Page Pagination `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` + Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` + Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` + Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` + Page *Pagination `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` } func (d DummyProto) Reset() {} @@ -154,7 +159,20 @@ func TestCreateMessage(t *testing.T) { Foo: "blah", Bar: true, Baz: 1352, - Page: Pagination{Limit: 3}, + Page: &Pagination{Limit: 3}, + }, + }, + { + name: "message with multi nested params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352", "page.limit": "3", "page.nest.foo": "5"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + Page: &Pagination{Limit: 3, Nest: &Nested{Foo: 5}}, }, }, { diff --git a/server/v2/go.mod b/server/v2/go.mod index 5d66e2e20e16..f1c0b45ce12e 100644 --- a/server/v2/go.mod +++ b/server/v2/go.mod @@ -15,6 +15,7 @@ require ( cosmossdk.io/core/testing v0.0.1 cosmossdk.io/log v1.5.0 cosmossdk.io/server/v2/appmanager v0.0.0-00010101000000-000000000000 + cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000 cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000 github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/gogogateway v1.2.0 @@ -33,6 +34,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.35.2 ) @@ -110,7 +112,6 @@ require ( golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect From 8369a89081ff587ce2761f3ae9f401fd772cee99 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:08:25 -0800 Subject: [PATCH 17/25] linter --- server/v2/api/grpcgateway/uri.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index 5ce024bd62eb..10452bbbfc9e 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -22,9 +22,11 @@ type uriMatch struct { // QueryInputName is the fully qualified name of the proto input type of the query rpc method. QueryInputName string - // Params are any wildcard params found in the request. + // Params are any wildcard/query params found in the request. // - // example: foo/bar/{baz} - foo/bar/qux -> {baz: qux} + // example: + // - foo/bar/{baz} - foo/bar/qux -> {baz: qux} + // - foo/bar?baz=qux - foo/bar -> {baz: qux} Params map[string]string } @@ -66,7 +68,7 @@ func matchURL(u *url.URL, getPatternToQueryInputName map[string]string) *uriMatc regex := regexp.MustCompile(regexPattern) matches := regex.FindStringSubmatch(uriPath) - if matches != nil && len(matches) > 1 { + if len(matches) > 1 { // first match is the full string, subsequent matches are capture groups for i, name := range wildcardNames { params[name] = matches[i+1] From 8b5aeb20c024992252ccc2c26ea75d3635eeae59 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:13:09 -0800 Subject: [PATCH 18/25] linter --- server/v2/api/grpcgateway/interceptor.go | 3 +-- server/v2/api/grpcgateway/uri.go | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index d604f7b92319..b9360653023b 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -10,9 +10,8 @@ import ( "google.golang.org/genproto/googleapis/api/annotations" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" "cosmossdk.io/core/transaction" "cosmossdk.io/log" diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go index 10452bbbfc9e..6531447cf889 100644 --- a/server/v2/api/grpcgateway/uri.go +++ b/server/v2/api/grpcgateway/uri.go @@ -131,7 +131,6 @@ func createMessageFromJSON(match *uriMatch, r *http.Request) (gogoproto.Message, } return msg, nil - } // createMessage creates a message from the given uriMatch. If the match has params, the message will be populated From ed18cb005e33a8ec6f4c138cfa917b53a86be7b9 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:25:49 -0800 Subject: [PATCH 19/25] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2113cf20b87..777c2ab5192c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i ### Features +* (server/v2) [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. * (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages. * (client/keys) [#21829](https://github.com/cosmos/cosmos-sdk/pull/21829) Add support for importing hex key using standard input. From e3551f0416fd22a854524bac711b0f9223a86f2f Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:26:05 -0800 Subject: [PATCH 20/25] changelog order --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 777c2ab5192c..e4bad1ce3551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,9 +42,9 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i ### Features -* (server/v2) [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. * (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages. * (client/keys) [#21829](https://github.com/cosmos/cosmos-sdk/pull/21829) Add support for importing hex key using standard input. +* (server/v2) [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. ### Improvements From 0b79fe42cae31769beb1b82f1378653d5d5a8536 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:05:53 -0800 Subject: [PATCH 21/25] use strings contains to check no handler error --- server/v2/api/grpcgateway/interceptor.go | 5 ++--- server/v2/go.mod | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go index b9360653023b..7119d452a9b9 100644 --- a/server/v2/api/grpcgateway/interceptor.go +++ b/server/v2/api/grpcgateway/interceptor.go @@ -1,9 +1,9 @@ package grpcgateway import ( - "errors" "net/http" "strconv" + "strings" gogoproto "github.com/cosmos/gogoproto/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,7 +16,6 @@ import ( "cosmossdk.io/core/transaction" "cosmossdk.io/log" "cosmossdk.io/server/v2/appmanager" - "cosmossdk.io/server/v2/stf" ) var _ http.Handler = &gatewayInterceptor[transaction.Tx]{} @@ -95,7 +94,7 @@ func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *h query, err := g.appManager.Query(request.Context(), height, msg) if err != nil { // if we couldn't find a handler for this request, just fall back to the gateway mux. - if errors.Is(err, stf.ErrNoHandler) { + if strings.Contains(err.Error(), "no handler") { g.gateway.ServeHTTP(writer, request) } else { // for all other errors, we just return the error. diff --git a/server/v2/go.mod b/server/v2/go.mod index f1c0b45ce12e..cbb19017ea9a 100644 --- a/server/v2/go.mod +++ b/server/v2/go.mod @@ -15,7 +15,6 @@ require ( cosmossdk.io/core/testing v0.0.1 cosmossdk.io/log v1.5.0 cosmossdk.io/server/v2/appmanager v0.0.0-00010101000000-000000000000 - cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000 cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000 github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/gogogateway v1.2.0 From ea4bdd86c07c54581d0f00c099d5c86d086247cb Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:06:07 -0800 Subject: [PATCH 22/25] remove todo --- server/v2/api/grpcgateway/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 343c28fb11c9..2aec6cad6387 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -73,8 +73,6 @@ func New[T transaction.Tx]( } } - // TODO: register the gRPC-Gateway routes - s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() From eb63921e25cd6b1794a678bdc9166908881aaa90 Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:06:12 -0800 Subject: [PATCH 23/25] remove changelog entry --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bad1ce3551..a2113cf20b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,6 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i * (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages. * (client/keys) [#21829](https://github.com/cosmos/cosmos-sdk/pull/21829) Add support for importing hex key using standard input. -* (server/v2) [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. ### Improvements From f0c3e47c1946c11cd962e559a7c754250b4e0e6a Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:14:53 -0800 Subject: [PATCH 24/25] add changelog to server/v2 --- server/v2/CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 server/v2/CHANGELOG.md diff --git a/server/v2/CHANGELOG.md b/server/v2/CHANGELOG.md new file mode 100644 index 000000000000..b6db8e4a66b0 --- /dev/null +++ b/server/v2/CHANGELOG.md @@ -0,0 +1,53 @@ + + +# Changelog + +## [Unreleased] + +Every module contains its own CHANGELOG.md. Please refer to the module you are interested in. + +### Features + +* [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. + +### Improvements + +### Bug Fixes + +### API Breaking Changes + +### Deprecated \ No newline at end of file From e3aeb4dbec5fd1a3faf78c79ecd9fcde3cdce5dc Mon Sep 17 00:00:00 2001 From: tyler <48813565+technicallyty@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:28:08 -0800 Subject: [PATCH 25/25] add error case to create message from json --- server/v2/api/grpcgateway/uri_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go index 063058505667..4bb74c39e40e 100644 --- a/server/v2/api/grpcgateway/uri_test.go +++ b/server/v2/api/grpcgateway/uri_test.go @@ -212,6 +212,7 @@ func TestCreateMessageFromJson(t *testing.T) { uri uriMatch request func() *http.Request expected gogoproto.Message + expErr bool }{ { name: "simple, empty message", @@ -240,12 +241,24 @@ func TestCreateMessageFromJson(t *testing.T) { Baz: 320, }, }, + { + name: "message with invalid json", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + return &http.Request{Body: io.NopCloser(bytes.NewReader([]byte(`{"foo":12,dfi3}"`)))} + }, + expErr: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actual, err := createMessageFromJSON(&tc.uri, tc.request()) - require.NoError(t, err) - require.Equal(t, tc.expected, actual) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } }) } }