diff --git a/fox.go b/fox.go index a9f9cbd..2186c52 100644 --- a/fox.go +++ b/fox.go @@ -12,6 +12,7 @@ import ( "net" "net/http" "path" + "regexp" "slices" "strings" "sync" @@ -975,15 +976,24 @@ func Sub(router *Router) HandlerFunc { *subCtx.subPatterns = append(*subCtx.subPatterns, *c.subPatterns...) - lastTkType := route.pattern.tokens[len(route.pattern.tokens)-1].typ + lastTk := route.pattern.tokens[len(route.pattern.tokens)-1] var p string - switch lastTkType { + switch lastTk.typ { case nodeWildcard: key := (*c.paramsKeys)[len(*c.paramsKeys)-1] - p = strings.TrimSuffix(c.pattern[:len(c.pattern)-(len(key)+wildcardExtraChar)], "/") + extra := len(key) + wildcardExtraChar + if lastTk.regexp != nil { + // +1 for the ':' separator between name and regex source. + extra += 1 + len(rawExpr(lastTk.regexp)) + } + p = strings.TrimSuffix(c.pattern[:len(c.pattern)-extra], "/") case nodeParam: key := (*c.paramsKeys)[len(*c.paramsKeys)-1] - p = strings.TrimSuffix(c.pattern[:len(c.pattern)-(len(key)+paramExtraChar)], "/") + extra := len(key) + paramExtraChar + if lastTk.regexp != nil { + extra += 1 + len(rawExpr(lastTk.regexp)) + } + p = strings.TrimSuffix(c.pattern[:len(c.pattern)-extra], "/") default: // Reaching this case means the parent route does not end with a catch-all parameter (e.g., /api/ // instead of /api/+{rest}). This is technically a misuse of the sub-router API, but we handle it @@ -1124,3 +1134,8 @@ func firstHeader(headers http.Header, k string) (string, bool) { } return v[0], true } + +func rawExpr(re *regexp.Regexp) string { + expr := re.String() + return expr[4 : len(expr)-2] +} diff --git a/fox_test.go b/fox_test.go index 5115739..50c6ffe 100644 --- a/fox_test.go +++ b/fox_test.go @@ -1478,6 +1478,34 @@ func TestRouter_ServeHTTP_HandleSubRouter(t *testing.T) { assert.Equal(t, http.StatusNotFound, w.Code) }) + t.Run("sub-router registered with regex wildcard", func(t *testing.T) { + sub := MustRouter() + sub.MustAdd(MethodGet, "/users", patternHandler) + + fx := MustRouter(AllowRegexpParam(true)) + require.NoError(t, onlyError(fx.Add(MethodGet, "/api/+{rest:[a-z]+}", Sub(sub)))) + + req := httptest.NewRequest(http.MethodGet, "/api/users", nil) + w := httptest.NewRecorder() + fx.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "/api/users", w.Body.String()) + }) + + t.Run("sub-router registered with regex param", func(t *testing.T) { + sub := MustRouter() + sub.MustAdd(MethodGet, "/users", patternHandler) + + fx := MustRouter(AllowRegexpParam(true)) + require.NoError(t, onlyError(fx.Add(MethodGet, "/api/{name:[a-z]+}", Sub(sub)))) + + req := httptest.NewRequest(http.MethodGet, "/api/users", nil) + w := httptest.NewRecorder() + fx.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "/api/users", w.Body.String()) + }) + t.Run("sub-router no route handler sees clean context", func(t *testing.T) { var pat, tenant string var route *Route diff --git a/matcher.go b/matcher.go index cf7158c..7366719 100644 --- a/matcher.go +++ b/matcher.go @@ -110,8 +110,7 @@ func (m QueryRegexpMatcher) Key() string { // Value returns the regular expression matching the query parameter. func (m QueryRegexpMatcher) Value() string { - expr := m.regex.String() - return expr[4 : len(expr)-2] + return rawExpr(m.regex) } // String returns a textual representation of the matcher in the form "qx:key=expr". @@ -224,8 +223,7 @@ func (m HeaderRegexpMatcher) Key() string { // Value returns the regular expression matching the header. func (m HeaderRegexpMatcher) Value() string { - expr := m.regex.String() - return expr[4 : len(expr)-2] + return rawExpr(m.regex) } // Match reports whether the request contains the configured header with any value matching the configured regular diff --git a/tree.go b/tree.go index f0cd4f4..ba9dac3 100644 --- a/tree.go +++ b/tree.go @@ -953,9 +953,7 @@ func concat(a, b string) string { // placeholder ("?" for params, "*" for catch-alls). func canonicalKey(tk token) string { if tk.regexp != nil { - // Strip the surrounding ^(?: and )$ added by compileParamRegexp. - expr := tk.regexp.String() - return expr[4 : len(expr)-2] + return rawExpr(tk.regexp) } switch tk.typ { case nodeParam: