diff --git a/fox.go b/fox.go index c8203b6..2c137ef 100644 --- a/fox.go +++ b/fox.go @@ -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) @@ -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) @@ -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 } diff --git a/node.go b/node.go index 942cce1..5e2028e 100644 --- a/node.go +++ b/node.go @@ -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, @@ -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, diff --git a/route.go b/route.go index 6e25220..10e807f 100644 --- a/route.go +++ b/route.go @@ -19,6 +19,7 @@ type Route struct { mws []middleware params []string matchers []Matcher + methodFast string pattern pattern priority uint handleSlash TrailingSlashOption @@ -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 } @@ -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 diff --git a/txn.go b/txn.go index 987064d..3139731 100644 --- a/txn.go +++ b/txn.go @@ -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) @@ -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)