Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(server/v2): auto-gateway improvements and doc #23262

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions server/v2/api/grpcgateway/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package grpcgateway provides a custom http mux that utilizes the global gogoproto registry to match
// grpc gateway requests to query handlers. POST requests with JSON bodies and GET requests with query params are supported.
// Wildcard endpoints (i.e. foo/bar/{baz}), as well as catch-all endpoints (i.e. foo/bar/{baz=**} are supported. Using
// header `x-cosmos-block-height` allows you to specify a height for the query.
//
// The URL matching logic is achieved by building regular expressions from the gateway HTTP annotations. These regular expressions
// are then used to match against incoming requests to the HTTP server.
//
// In cases where the custom http mux is unable to handle the query (i.e. no match found), the request will fall back to the
// ServeMux from github.com/grpc-ecosystem/grpc-gateway/runtime.
package grpcgateway
44 changes: 36 additions & 8 deletions server/v2/api/grpcgateway/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"net/http"
"regexp"
"strconv"
"strings"

Expand All @@ -20,16 +21,28 @@

var _ http.Handler = &gatewayInterceptor[transaction.Tx]{}

// queryMetadata holds information related to handling gateway queries.
type queryMetadata struct {
// queryInputProtoName is the proto name of the query's input type.
queryInputProtoName string
// wildcardKeyNames are the wildcard key names from the query's HTTP annotation.
// for example /foo/bar/{baz}/{qux} would produce []string{"baz", "qux"}
// this is used for building the query's parameter map.
wildcardKeyNames []string
}

// 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

// customEndpointMapping is a mapping of custom GET options on proto RPC handlers, to the fully qualified method name.
// regexpToQueryMetadata is a mapping of regular expressions of HTTP annotations to metadata for the query.
// it is built from parsing the HTTP annotations obtained from the gogoproto global registry.'
//
// example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata
customEndpointMapping map[string]string
// TODO: it might be interesting to make this a 'most frequently used' data structure, so frequently used regexp's are
// iterated over first.
regexpToQueryMetadata map[*regexp.Regexp]queryMetadata

// appManager is used to route queries to the application.
appManager appmanager.AppManager[T]
Expand All @@ -41,20 +54,20 @@
if err != nil {
return nil, err
}
regexQueryMD := createRegexMapping(getMapping)
return &gatewayInterceptor[T]{
logger: logger,
gateway: gateway,
customEndpointMapping: getMapping,
regexpToQueryMetadata: regexQueryMD,
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.
// ServeHTTP implements the http.Handler interface. This method will attempt to match request URIs to its internal mapping
// of gateway HTTP annotations. 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)
match := matchURL(request.URL, g.regexpToQueryMetadata)
if match == nil {
// no match cases fall back to gateway mux.
g.gateway.ServeHTTP(writer, request)
Expand Down Expand Up @@ -143,3 +156,18 @@

return httpGets, nil
}

// createRegexMapping converts the annotationMapping (HTTP annotation -> query input type name) to a
// map of regular expressions for that HTTP annotation pattern, to queryMetadata.
func createRegexMapping(annotationMapping map[string]string) map[*regexp.Regexp]queryMetadata {
regexQueryMD := make(map[*regexp.Regexp]queryMetadata)
for annotation, queryInputName := range annotationMapping {
pattern, wildcardNames := patternToRegex(annotation)
reg := regexp.MustCompile(pattern)
regexQueryMD[reg] = queryMetadata{
queryInputProtoName: queryInputName,
wildcardKeyNames: wildcardNames,
}
}
Comment on lines +164 to +171

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism
return regexQueryMD
}
64 changes: 27 additions & 37 deletions server/v2/api/grpcgateway/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
// Params are any wildcard/query params found in the request.
//
// example:
// - foo/bar/{baz} - foo/bar/qux -> {baz: qux}
// - foo/bar?baz=qux - foo/bar -> {baz: qux}
Params map[string]string
// - foo/bar/{baz} - foo/bar/qux -> {baz: {"qux"}}
// - foo/bar?baz=qux - foo/bar -> {baz: {"qux"}
// - foo/bar?denom=atom&denom=stake -> {denom: {"atom", "stake"}}
Params map[string][]string
}

// HasParams reports whether the uriMatch has any params.
Expand All @@ -37,50 +38,36 @@

// matchURL attempts to find a match for the given URL.
// NOTE: if no match is found, nil is returned.
func matchURL(u *url.URL, getPatternToQueryInputName map[string]string) *uriMatch {
func matchURL(u *url.URL, regexpToQueryMetadata map[*regexp.Regexp]queryMetadata) *uriMatch {
uriPath := strings.TrimRight(u.Path, "/")
queryParams := u.Query()

params := make(map[string]string)
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]
params[key] = vals
}
}

// for simple cases where there are no wildcards, we can just do a map lookup.
if inputName, ok := getPatternToQueryInputName[uriPath]; ok {
return &uriMatch{
QueryInputName: inputName,
Params: params,
}
}

// attempt to find a match in the pattern map.
for getPattern, queryInputName := range getPatternToQueryInputName {
getPattern = strings.TrimRight(getPattern, "/")

regexPattern, wildcardNames := patternToRegex(getPattern)

regex := regexp.MustCompile(regexPattern)
matches := regex.FindStringSubmatch(uriPath)

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]
for reg, qmd := range regexpToQueryMetadata {
matches := reg.FindStringSubmatch(uriPath)
switch {
case len(matches) == 1:
return &uriMatch{
QueryInputName: qmd.queryInputProtoName,
Params: params,
}
case len(matches) > 1:
// first match is the URI, subsequent matches are the wild card values.
for i, name := range qmd.wildcardKeyNames {
params[name] = []string{matches[i+1]}
}

return &uriMatch{
QueryInputName: queryInputName,
QueryInputName: qmd.queryInputProtoName,
Params: params,
}
}
}

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism

return nil
}

Expand Down Expand Up @@ -148,26 +135,29 @@

// if the uri match has params, we need to populate the message with the values of those params.
if match.HasParams() {
// convert flat params map to nested structure
nestedParams := make(map[string]any)
for key, value := range match.Params {
for key, values := range match.Params {
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
// Last part - set the value(s)
if len(values) == 1 {
current[part] = values[0] // single value
} else {
current[part] = values // slice of values
}
} else {
// continue nestedness
if _, exists := current[part]; !exists {
current[part] = make(map[string]any)
}
current = current[part].(map[string]any)
}
}
}

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism

// Configure decoder to handle the nested structure
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Expand Down
Loading
Loading