Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions fox.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ func (fox *Router) Name(name string) *Route {
// (trailing slash action recommended). This function is safe for concurrent use by multiple goroutine and while
// mutation on routes are ongoing. See also [Router.Lookup] as an alternative.
func (fox *Router) Match(method string, r *http.Request) (route *Route, tsr bool) {
if method == "" {
return nil, false
}
tree := fox.getTree()
c := tree.pool.Get().(*Context)
defer tree.pool.Put(c)
Expand All @@ -381,6 +384,9 @@ func (fox *Router) Match(method string, r *http.Request) (route *Route, tsr bool
// [Route] and a [Context]. The [Context] should always be closed if non-nil. This function is safe for
// concurrent use by multiple goroutine and while mutation on routes are ongoing. See also [Router.Match] as an alternative.
func (fox *Router) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc *Context, tsr bool) {
if r.Method == "" {
return nil, nil, false
}
tree := fox.getTree()
c := tree.pool.Get().(*Context)
c.resetWithWriter(w, r)
Expand Down Expand Up @@ -459,6 +465,10 @@ func (fox *Router) NewRoute(methods []string, pattern string, handler HandlerFun
rte.methods = slices.Compact(rte.methods)
}

if len(rte.methods) == 1 && len(rte.matchers) == 0 {
rte.methodFast = rte.methods[0]
}

return rte, nil
}

Expand Down
11 changes: 8 additions & 3 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ Walk:
if idx < num && matched.statics[idx].label == label {
child := matched.statics[idx]
keyLen := len(child.key)
if keyLen <= len(search) && stringsutil.EqualStringsASCIIIgnoreCase(search[:keyLen], child.key) {
// keyLen == 1 short-circuits the case-insensitive compare: hostname keys are
// stored lowercase (uppercase is rejected at parse time) and the label match
// already proved lowercase(search[0]) == child.key[0].
if keyLen == 1 || (keyLen <= len(search) && stringsutil.EqualStringsASCIIIgnoreCase(search[:keyLen], child.key)) {
if len(matched.params) > 0 || len(matched.wildcards) > 0 {
*c.skipStack = append(*c.skipStack, skipNode{
node: matched,
Expand Down Expand Up @@ -332,8 +335,10 @@ Walk:
child := matched.statics[idx]
keyLen := len(child.key)
// While this is less performant than byte-by-byte comparaison for reasonable search size,
// direct == comparaison on string scale way better on long route.
if keyLen <= len(search) && search[:keyLen] == child.key {
// direct == comparaison on string scale way better on long route. The keyLen == 1 case
// short-circuits the memequal call: we already verified search[0] == child.key[0] via the
// label match above.
if keyLen == 1 || (keyLen <= len(search) && search[:keyLen] == child.key) {
if len(matched.params) > 0 || len(matched.wildcards) > 0 {
*c.skipStack = append(*c.skipStack, skipNode{
node: matched,
Expand Down
21 changes: 18 additions & 3 deletions route.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Route struct {
mws []middleware
params []string
matchers []Matcher
methodFast string
pattern pattern
priority uint
handleSlash TrailingSlashOption
Expand Down Expand Up @@ -135,12 +136,27 @@ func (r *Route) String() string {
// match reports whether the request satisfies this route's method constraint (if any)
// and all attached matchers.
func (r *Route) match(method string, c RequestContext) bool {
// Fast path for common cases: no methods or single method
// Fast path: routes with exactly one method and no matchers cache that
// method in methodFast.
if r.methodFast == method {
return true
}
return r.matchSlow(method, c)
}

// matchSlow handles the cases match's fast path does not cover: zero or many
// methods, and routes with matchers. It is kept out-of-line so match remains
// inlinable.
//
//go:noinline
func (r *Route) matchSlow(method string, c RequestContext) bool {
methods := r.methods
switch len(methods) {
case 0:
// No method constraint
// No method constraint.
case 1:
// Avoid the slices.Contains overhead for the single-method case (which
// match's fast path leaves to us when matchers are present).
if methods[0] != method {
return false
}
Expand All @@ -149,7 +165,6 @@ func (r *Route) match(method string, c RequestContext) bool {
return false
}
}

for _, m := range r.matchers {
if !m.Match(c) {
return false
Expand Down
8 changes: 8 additions & 0 deletions txn.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ func (txn *Txn) Match(method string, r *http.Request) (route *Route, tsr bool) {
panic(ErrSettledTxn)
}

if method == "" {
return nil, false
}

tree := txn.rootTxn.tree
c := tree.pool.Get().(*Context)
defer tree.pool.Put(c)
Expand All @@ -309,6 +313,10 @@ func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc *Con
panic(ErrSettledTxn)
}

if r.Method == "" {
return nil, nil, false
}

tree := txn.rootTxn.tree
c := tree.pool.Get().(*Context)
c.resetWithWriter(w, r)
Expand Down
Loading