From 99c032646e112ee7671c2af89b530d1ac15981c9 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 14 Feb 2026 02:43:33 -0800 Subject: [PATCH] fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath enabled When RedirectFixedPath is enabled and a request doesn't match any route, findCaseInsensitivePathRec panics with "invalid node type". This happens because it accesses children[0] to find the wildcard child, but addChild() keeps the wildcard child at the end of the array (not the beginning). When a node has both static and wildcard children, children[0] is a static node, so the switch on nType falls through to the default panic case. The fix mirrors what getValue() already does correctly (line 483): use children[len(children)-1] to access the wildcard child. Fixes #2959 --- tree.go | 3 +- tree_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/tree.go b/tree.go index 3ac0a3b1..a15e0b42 100644 --- a/tree.go +++ b/tree.go @@ -818,7 +818,8 @@ walk: // Outer loop for walking the tree return nil } - n = n.children[0] + // Handle wildcard child, which is always at the end of the array + n = n.children[len(n.children)-1] switch n.nType { case param: // Find param end (either '/' or path end) diff --git a/tree_test.go b/tree_test.go index b580007d..8cc55f77 100644 --- a/tree_test.go +++ b/tree_test.go @@ -1018,3 +1018,102 @@ func TestWildcardInvalidSlash(t *testing.T) { } } } + +func TestTreeFindCaseInsensitivePathWithMultipleChildrenAndWildcard(t *testing.T) { + tree := &node{} + + // Setup routes that create a node with both static children and a wildcard child. + // This configuration previously caused a panic ("invalid node type") in + // findCaseInsensitivePathRec because it accessed children[0] instead of the + // wildcard child (which is always at the end of the children array). + // See: https://github.com/gin-gonic/gin/issues/2959 + routes := [...]string{ + "/aa/aa", + "/:bb/aa", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // These lookups previously panicked with "invalid node type" because + // findCaseInsensitivePathRec picked children[0] (a static node) instead + // of the wildcard child at the end of the array. + recv := catchPanic(func() { + tree.findCaseInsensitivePath("/aa", true) + }) + if recv != nil { + t.Fatalf("unexpected panic looking up '/aa': %v", recv) + } + + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/aa/aa/aa/aa", true) + }) + if recv != nil { + t.Fatalf("unexpected panic looking up '/aa/aa/aa/aa': %v", recv) + } + + // Also test case-insensitive lookup (this was crashing too) + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/AA/AA", true) + }) + if recv != nil { + t.Fatalf("unexpected panic looking up '/AA/AA': %v", recv) + } +} + +func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) { + tree := &node{} + + // Another variant: param route + static route under same prefix + routes := [...]string{ + "/prefix/:id", + "/prefix/xxx", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Should NOT panic even for paths that don't match any route + recv := catchPanic(func() { + tree.findCaseInsensitivePath("/prefix/a/b/c", true) + }) + if recv != nil { + t.Fatalf("unexpected panic looking up '/prefix/a/b/c': %v", recv) + } + + // Exact match should still work + out, found := tree.findCaseInsensitivePath("/prefix/xxx", true) + if !found { + t.Error("Route '/prefix/xxx' not found") + } else if string(out) != "/prefix/xxx" { + t.Errorf("Wrong result for '/prefix/xxx': %s", string(out)) + } + + // Case-insensitive match should work + out, found = tree.findCaseInsensitivePath("/PREFIX/XXX", true) + if !found { + t.Error("Route '/PREFIX/XXX' not found via case-insensitive lookup") + } else if string(out) != "/prefix/XXX" { + t.Errorf("Wrong result for '/PREFIX/XXX': %s", string(out)) + } + + // Param route should still match + out, found = tree.findCaseInsensitivePath("/prefix/something", true) + if !found { + t.Error("Route '/prefix/something' not found via param match") + } else if string(out) != "/prefix/something" { + t.Errorf("Wrong result for '/prefix/something': %s", string(out)) + } +}