Skip to content

Commit

Permalink
fix: simple matchers for URIs that are similar to wildcards
Browse files Browse the repository at this point in the history
  • Loading branch information
technicallyty committed Jan 13, 2025
1 parent 02deaf9 commit d909cd9
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 22 deletions.
42 changes: 26 additions & 16 deletions server/v2/api/grpcgateway/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@ type gatewayInterceptor[T transaction.Tx] struct {
// gateway is the fallback grpc gateway mux handler.
gateway *runtime.ServeMux

// 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.'
//
// 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
matcher uriMatcher

// appManager is used to route queries to the application.
appManager appmanager.AppManager[T]
Expand All @@ -63,23 +58,27 @@ func newGatewayInterceptor[T transaction.Tx](logger log.Logger, gateway *runtime
return nil, err
}
// convert the mapping to regular expressions for URL matching.
regexQueryMD := createRegexMapping(logger, getMapping)
wildcardMatchers, simpleMatchers := createRegexMapping(logger, getMapping)
if err != nil {
return nil, err
}
matcher := uriMatcher{
wildcardURIMatchers: wildcardMatchers,
simpleMatchers: simpleMatchers,
}
return &gatewayInterceptor[T]{
logger: logger,
gateway: gateway,
regexpToQueryMetadata: regexQueryMD,
appManager: am,
logger: logger,
gateway: gateway,
matcher: matcher,
appManager: am,
}, nil
}

// 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.regexpToQueryMetadata)
match := g.matcher.matchURL(request.URL)
if match == nil {
// no match cases fall back to gateway mux.
g.gateway.ServeHTTP(writer, request)
Expand Down Expand Up @@ -228,11 +227,22 @@ func getHTTPGetAnnotationMapping() (map[string]string, error) {

// 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(logger log.Logger, annotationMapping map[string]string) map[*regexp.Regexp]queryMetadata {
regexQueryMD := make(map[*regexp.Regexp]queryMetadata)
func createRegexMapping(logger log.Logger, annotationMapping map[string]string) (map[*regexp.Regexp]queryMetadata, map[string]queryMetadata) {
wildcardMatchers := make(map[*regexp.Regexp]queryMetadata)
// seen patterns is a map of URI patterns to annotations. for simple queries (no wildcards) the annotation is used
// for the key.
seenPatterns := make(map[string]string)
simpleMatchers := make(map[string]queryMetadata)

for annotation, queryInputName := range annotationMapping {
pattern, wildcardNames := patternToRegex(annotation)
if len(wildcardNames) == 0 {
simpleMatchers[annotation] = queryMetadata{
queryInputProtoName: queryInputName,
wildcardKeyNames: nil,
}
seenPatterns[annotation] = annotation
}
reg := regexp.MustCompile(pattern)
if otherAnnotation, ok := seenPatterns[pattern]; !ok {
seenPatterns[pattern] = annotation
Expand All @@ -241,10 +251,10 @@ func createRegexMapping(logger log.Logger, annotationMapping map[string]string)
// see: https://github.com/cosmos/cosmos-sdk/issues/23281
logger.Warn("duplicate HTTP annotation found", "annotation1", annotation, "annotation2", otherAnnotation, "query_input_name", queryInputName)
}
regexQueryMD[reg] = queryMetadata{
wildcardMatchers[reg] = queryMetadata{
queryInputProtoName: queryInputName,
wildcardKeyNames: wildcardNames,
}
}
return regexQueryMD
return wildcardMatchers, simpleMatchers
}
2 changes: 1 addition & 1 deletion server/v2/api/grpcgateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func New[T transaction.Tx](
// marshaled in unary requests.
runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),

// Custom header matcher for mapping request headers to
// Custom header uriMatcher for mapping request headers to
// GRPC metadata
runtime.WithIncomingHeaderMatcher(CustomGRPCHeaderMatcher),
),
Expand Down
21 changes: 19 additions & 2 deletions server/v2/api/grpcgateway/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import (
"strings"
)

// uriMatcher provides functionality to match HTTP request URIs.
type uriMatcher struct {
// wildcardURIMatchers are used for complex URIs that involve wildcards (i.e. /foo/{bar}/baz)
wildcardURIMatchers map[*regexp.Regexp]queryMetadata
// simpleMatchers are used for simple URI's that have no wildcards (i.e. /foo/bar/baz).
simpleMatchers map[string]queryMetadata
}

// 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.
Expand All @@ -19,11 +27,20 @@ type uriMatch struct {

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

for reg, qmd := range regexpToQueryMetadata {
// see if we can get a simple match first.
if qmd, ok := m.simpleMatchers[uriPath]; ok {
return &uriMatch{
QueryInputName: qmd.queryInputProtoName,
Params: params,
}
}

// try the complex matchers.
for reg, qmd := range m.wildcardURIMatchers {
matches := reg.FindStringSubmatch(uriPath)
switch {
case len(matches) == 1:
Expand Down
29 changes: 26 additions & 3 deletions server/v2/api/grpcgateway/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ func TestMatchURI(t *testing.T) {
mapping: map[string]string{"/foo/bar": "query.Bank"},
expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{}},
},
{
name: "match with wildcard similar to simple match - simple",
uri: "https://localhost:8080/bank/supply/latest",
mapping: map[string]string{
"/bank/supply/{height}": "queryBankHeight",
"/bank/supply/latest": "queryBankLatest",
},
expected: &uriMatch{QueryInputName: "queryBankLatest", Params: map[string]string{}},
},
{
name: "match with wildcard similar to simple match - wildcard",
uri: "https://localhost:8080/bank/supply/52",
mapping: map[string]string{
"/bank/supply/{height}": "queryBankHeight",
"/bank/supply/latest": "queryBankLatest",
},
expected: &uriMatch{QueryInputName: "queryBankHeight", Params: map[string]string{"height": "52"}},
},
{
name: "wildcard match at the end",
uri: "https://localhost:8080/foo/bar/buzz",
Expand Down Expand Up @@ -73,9 +91,14 @@ func TestMatchURI(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
u, err := url.Parse(tc.uri)
require.NoError(t, err)
regexpMapping := createRegexMapping(logger, tc.mapping)
require.NoError(t, err)
actual := matchURL(u, regexpMapping)

regexpMatchers, simpleMatchers := createRegexMapping(logger, tc.mapping)
matcher := uriMatcher{
wildcardURIMatchers: regexpMatchers,
simpleMatchers: simpleMatchers,
}

actual := matcher.matchURL(u)
require.Equal(t, tc.expected, actual)
})
}
Expand Down

0 comments on commit d909cd9

Please sign in to comment.