diff --git a/context_test.go b/context_test.go index 364a92ae..f3751891 100644 --- a/context_test.go +++ b/context_test.go @@ -3868,3 +3868,56 @@ func BenchmarkGetMapFromFormData(b *testing.B) { }) } } + +func TestWildcardParamUnicodeConcurrency(t *testing.T) { + router := New() + + var mu sync.Mutex + var errs []string + + router.GET("/user/:name", func(c *Context) { + name := c.Param("name") + if name == "" { + mu.Lock() + errs = append(errs, "name param is empty") + mu.Unlock() + } + }) + + router.GET("/files/*filepath", func(c *Context) { + filepath := c.Param("filepath") + if filepath == "" { + mu.Lock() + errs = append(errs, "filepath param is empty") + mu.Unlock() + } + }) + + var wg sync.WaitGroup + paths := []string{ + "/user/जयेश", + "/files/🎉/photo.png", + "/user/こんにちは", + } + + for i := 0; i < 20; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for _, p := range paths { + req, _ := http.NewRequest(http.MethodGet, p, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + mu.Lock() + errs = append(errs, "status code is not 200") + mu.Unlock() + } + } + }() + } + wg.Wait() + + assert.Empty(t, errs) +} diff --git a/go.mod b/go.mod index df181253..c67c5fed 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/quic-go/quic-go v0.59.0 + github.com/quic-go/quic-go v0.59.1 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver/v2 v2.5.0 diff --git a/go.sum b/go.sum index f7f9e27b..e69bfeee 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/tree.go b/tree.go index 580abbaf..f12d3b5c 100644 --- a/tree.go +++ b/tree.go @@ -509,6 +509,11 @@ walk: // Outer loop for walking the tree // Expand slice within preallocated capacity i := len(*value.params) *value.params = (*value.params)[:i+1] + + // Ensure 'end' index lands exactly on a valid UTF-8 rune boundary + for end > 0 && end < len(path) && !utf8.RuneStart(path[end]) { + end-- + } val := path[:end] if unescape { if v, err := url.QueryUnescape(val); err == nil { diff --git a/tree_test.go b/tree_test.go index 23339af4..70cc4f4b 100644 --- a/tree_test.go +++ b/tree_test.go @@ -1111,3 +1111,20 @@ func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) { t.Errorf("Wrong result for '/prefix/something': %s", string(out)) } } + +func TestTreeWildcardParamImproperBoundaryCoverage(t *testing.T) { + tree := &node{} + + // Register a path with a wild named parameter segment + tree.addRoute("/submit/:info", HandlersChain{func(c *Context) {}}) + + // Pass a path containing a multi-byte sequence where a standard byte segment lookup + // drifts directly into the middle of a continuation block. + // This exercises our inner boundary alignment decrement loop. + path := "/submit/जय" + value := tree.getValue(path, &Params{}, nil, false) + + if value.handlers == nil { + t.Errorf("Routing fallback failed on multi-byte parameter verification evaluation.") + } +}