diff --git a/tree.go b/tree.go index a15e0b42..580abbaf 100644 --- a/tree.go +++ b/tree.go @@ -818,7 +818,71 @@ walk: // Outer loop for walking the tree return nil } - // Handle wildcard child, which is always at the end of the array + // When wildChild is true, try static children first (via indices) + // before falling back to the wildcard child. This ensures that + // case-insensitive lookups prefer static routes over param routes + // (e.g., /PREFIX/XXX should resolve to /prefix/xxx, not match :id). + if len(n.indices) > 0 { + rb = shiftNRuneBytes(rb, npLen) + + if rb[0] != 0 { + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + } else { + var rv rune + var off int + for max_ := min(npLen, 3); off < max_; off++ { + if i := npLen - off; utf8.RuneStart(oldPath[i]) { + rv, _ = utf8.DecodeRuneInString(oldPath[i:]) + break + } + } + + lo := unicode.ToLower(rv) + utf8.EncodeRune(rb[:], lo) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + + if up := unicode.ToUpper(rv); up != lo { + utf8.EncodeRune(rb[:], up) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + } + } + } + + // Fall back to wildcard child, which is always at the end of the array n = n.children[len(n.children)-1] switch n.nType { case param: diff --git a/tree_test.go b/tree_test.go index 8cc55f77..23339af4 100644 --- a/tree_test.go +++ b/tree_test.go @@ -1044,26 +1044,22 @@ func TestTreeFindCaseInsensitivePathWithMultipleChildrenAndWildcard(t *testing.T // 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) + out, found := tree.findCaseInsensitivePath("/aa", true) + if found { + t.Errorf("Expected no match for '/aa', but got: %s", string(out)) } - 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) + out, found = tree.findCaseInsensitivePath("/aa/aa/aa/aa", true) + if found { + t.Errorf("Expected no match for '/aa/aa/aa/aa', but got: %s", string(out)) } - // 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) + // Case-insensitive lookup should match the static route /aa/aa + out, found = tree.findCaseInsensitivePath("/AA/AA", true) + if !found { + t.Error("Route '/AA/AA' not found via case-insensitive lookup") + } else if string(out) != "/aa/aa" { + t.Errorf("Wrong result for '/AA/AA': expected '/aa/aa', got: %s", string(out)) } } @@ -1086,15 +1082,13 @@ func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) { } // 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) + out, found := tree.findCaseInsensitivePath("/prefix/a/b/c", true) + if found { + t.Errorf("Expected no match for '/prefix/a/b/c', but got: %s", string(out)) } // Exact match should still work - out, found := tree.findCaseInsensitivePath("/prefix/xxx", true) + out, found = tree.findCaseInsensitivePath("/prefix/xxx", true) if !found { t.Error("Route '/prefix/xxx' not found") } else if string(out) != "/prefix/xxx" { @@ -1105,8 +1099,8 @@ func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) { 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)) + } else if string(out) != "/prefix/xxx" { + t.Errorf("Wrong result for '/PREFIX/XXX': expected '/prefix/xxx', got: %s", string(out)) } // Param route should still match