From b2da23f506b31b242e4b9ad7ef14ef115b308b2d Mon Sep 17 00:00:00 2001 From: greymoth-jp Date: Fri, 26 Jun 2026 03:27:17 +0900 Subject: [PATCH] fix: match regexp params whose value contains the tail delimiter A regexp param can legitimately capture a value that includes the byte used as the delimiter before the next part of the same path segment, for example a UUID ("8-4-4-4-12") followed by a literal "-" and another param: /test/{uuid:[0-9a-f]{8}-...-[0-9a-f]{12}}-{date}. findRoute split the search string at the first occurrence of the tail delimiter, which truncated the value (e.g. to the first 8 hex chars) so the anchored regexp never matched and the route fell through to 404. For regexp param nodes, advance to subsequent occurrences of the tail delimiter within the same path segment until the regexp matches, instead of giving up after the first. Non-regexp params and cross-segment guards are unchanged. Fixes #970 --- tree.go | 24 +++++++++++++++++++++++- tree_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/tree.go b/tree.go index 95f31d4f..aed2e50c 100644 --- a/tree.go +++ b/tree.go @@ -446,7 +446,29 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { } if ntyp == ntRegexp && xn.rex != nil { - if !xn.rex.MatchString(xsearch[:p]) { + // The tail delimiter can legitimately appear inside the value a + // regexp param is meant to capture (e.g. a "-" delimiter with a + // UUID value). Splitting on the first delimiter would truncate the + // value and fail to match, so advance to subsequent delimiters + // within the same path segment until the regexp matches. + for !xn.rex.MatchString(xsearch[:p]) { + next := -1 + if p < len(xsearch) { + next = strings.IndexByte(xsearch[p+1:], xn.tail) + } + if next < 0 { + p = -1 + break + } + np := p + 1 + next + // don't let a regexp param match across path segments + if strings.IndexByte(xsearch[:np], '/') != -1 { + p = -1 + break + } + p = np + } + if p < 0 { continue } } else if strings.IndexByte(xsearch[:p], '/') != -1 { diff --git a/tree_test.go b/tree_test.go index c07e8309..a1d515f9 100644 --- a/tree_test.go +++ b/tree_test.go @@ -414,6 +414,50 @@ func TestTreeRegexMatchWholeParam(t *testing.T) { } } +// A regexp param can legitimately capture a value that contains the byte used +// as the tail delimiter for the next part of the same path segment (e.g. a UUID +// "8-4-4-4-12" value followed by a literal "-" then another param). The router +// must not truncate the value at the first delimiter. See issue #970. +func TestTreeRegexpTailInValue(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + rctx := NewRouteContext() + tr := &node{} + // {uuid} contains '-', which is also the delimiter before {date}. + tr.InsertRoute(mGET, "/test/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}-{date}", hStub1) + + tests := []struct { + url string + expectedHandler http.Handler + uuid string + date string + }{ + {url: "/test/f9772163-44e7-49b1-9c8c-36b8d023aa6b-2024-01-01", expectedHandler: hStub1, uuid: "f9772163-44e7-49b1-9c8c-36b8d023aa6b", date: "2024-01-01"}, + // value that does not satisfy the regexp must not match + {url: "/test/notauuid-2024-01-01", expectedHandler: nil}, + // missing trailing {date} segment must not match + {url: "/test/f9772163-44e7-49b1-9c8c-36b8d023aa6b", expectedHandler: nil}, + } + + for _, tc := range tests { + rctx.Reset() + _, _, handler := tr.FindRoute(rctx, mGET, tc.url) + if fmt.Sprintf("%v", tc.expectedHandler) != fmt.Sprintf("%v", handler) { + t.Errorf("url %v: expecting handler:%v , got:%v", tc.url, tc.expectedHandler, handler) + continue + } + if tc.expectedHandler == nil { + continue + } + if got := rctx.URLParam("uuid"); got != tc.uuid { + t.Errorf("url %v: uuid = %q, want %q", tc.url, got, tc.uuid) + } + if got := rctx.URLParam("date"); got != tc.date { + t.Errorf("url %v: date = %q, want %q", tc.url, got, tc.date) + } + } +} + func TestTreeFindPattern(t *testing.T) { hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})