Repository: gorilla/mux
Pull request #731 (2023-08-24 12:21:08)

Add RegexpCompileFunc to override regexp.Compile
## What type of PR is this? (check all applicable)

- [x] Refactor
- [x] Feature
- [ ] Bug Fix
- [x] Optimization
- [ ] Documentation Update

## Description

Route creation is slow as it always invokes regexp.Compile, increasing memory and cpu pressure in cases where route creation is common (e.g. an api gateway reloading changes). This PR adds a exported `RegexpCompileFunc` variable, that's possible to override with other implementations, adding caches. Added a benchmark.

## Related Tickets & Documents

```
# go test -bench=NewRouter -benchmem -memprofile memprofile.out -cpuprofile profile.out -count 10
goos: linux
goarch: amd64
pkg: github.com/gorilla/mux
cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
BenchmarkNewRouter-8             	   36786	     34450 ns/op	   26327 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   44839	     31840 ns/op	   26327 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   48939	     23419 ns/op	   26327 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   52255	     25949 ns/op	   26327 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   44684	     23150 ns/op	   26329 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   41522	     35047 ns/op	   26328 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   48354	     35461 ns/op	   26329 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   33850	     38467 ns/op	   26330 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   36920	     33917 ns/op	   26330 B/op	     374 allocs/op
BenchmarkNewRouter-8             	   40767	     29980 ns/op	   26329 B/op	     374 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  303121	      3738 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  311847	      4184 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  300144	      4212 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  329636	      4528 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  277980	      5136 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  363416	      4578 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  328996	      4231 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  327628	      3699 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  300370	      3955 ns/op	    2161 B/op	      73 allocs/op
BenchmarkNewRouterRegexpFunc-8   	  297345	      4115 ns/op	    2161 B/op	      73 allocs/op
PASS
ok  	github.com/gorilla/mux	35.091s
```

Benchmark: NewRouter

Average runtime per iteration: ~34450 ns/op
Average memory usage per iteration: ~26327 B/op
Average allocations per iteration: ~374 allocs/op

Benchmark: NewRouterRegexpFunc

Average runtime per iteration: ~4184 ns/op (approx. 87.8% improvement)
Average memory usage per iteration: ~2161 B/op (approx. 91.8% reduction)
Average allocations per iteration: ~73 allocs/op (approx. 80.5% reduction)

As per go doc, [using *regexp.Regexp concurrently is safe](https://pkg.go.dev/regexp#Regexp).

## Added/updated tests?

- [x] Yes
- [ ] No, and this is why: _please replace this line with details on why tests
      have not been included_
- [ ] I need help with writing tests

## Run verifications and test

- [ ] `make verify` is passing
- [x] `make test` is passing


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gorilla/mux/pull/731?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) Report
> Merging [#731](https://app.codecov.io/gh/gorilla/mux/pull/731?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) (1e42894) into [main](https://app.codecov.io/gh/gorilla/mux/commit/85123bf20e069b156415b871dea10517f6a8938a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) (85123bf) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main     #731   +/-   ##
=======================================
  Coverage   78.01%   78.01%           
=======================================
  Files           5        5           
  Lines         887      887           
=======================================
  Hits          692      692           
  Misses        140      140           
  Partials       55       55           
```


| [Files Changed](https://app.codecov.io/gh/gorilla/mux/pull/731?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) | Coverage Δ | |
|---|---|---|
| [mux.go](https://app.codecov.io/gh/gorilla/mux/pull/731?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla#diff-bXV4Lmdv) | `83.52% <100.00%> (ø)` | |
| [regexp.go](https://app.codecov.io/gh/gorilla/mux/pull/731?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla#diff-cmVnZXhwLmdv) | `83.05% <100.00%> (ø)` | |


coreydaley: @titpetric Thank you for the pull request! We will need a little bit of time to digest this and review it, but it is definitely towards the top of our queue.
titpetric: @coreydaley Let me know if there's absolutely anything I can do to improve it. I'm on a weird precipice where CSEs are routing for using the internal fork and I'd like to avoid that.

I've considered adding a .SetRegexpCompile(func(...)...) as an API addition, but wen't with the 'less is more' approach. It would be possible to update this in runtime with some mutex protections. That sounds fine until you consider how many extra bugs can rise up when conccurency is at play.

Explicit control of the caching behaviour is a single shot - if someone wants to control that during the router lifetime, just implementing the compile func signature enables it. It's expected there would be some sort of smarter eviction implementations behind it. Overthinking it just leads to more unnecessary problems, keeping things simple is hard sometimes.
AlexVulaj: @coreydaley This looks good to me, though I'll defer to you for a merge since I know you were looking at it first.

While I'm in favor of giving consumers the freedom to optimize their route creation, I believe such an action comes with the implicit disclaimer that if someone is going to play with around with it, they should know what they're doing and are on their own if it breaks something.
AlexVulaj: Another note - all of the changes from this PR seem to also be present in https://github.com/gorilla/mux/pull/732
titpetric: > Another note - all of the changes from this PR seem to also be present in #732

Correct, regexp is it's own optimization scope (this PR). The second PR needs a rebase when this is merged, that addresses actual memory pressure, while this one just gives us an ability to use a cached/compiled *regexp.Regexp.
titpetric: - rebased
- marked testing.TB internal func param as `_` (leave in place)

Patch:
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
index 8be6ced..768b05b 100644
--- a/.github/workflows/issues.yml
+++ b/.github/workflows/issues.yml
@@ -1,4 +1,4 @@
-# Add issues or pull-requests created to the project. 
+# Add all the issues created to the project.
 name: Add issue or pull request to Project
 
 on:
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 0000000..ff4a613
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,37 @@
+name: Security
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+permissions:
+  contents: read
+jobs:
+  scan:
+    strategy:
+      matrix:
+        go: ['1.20','1.21']
+      fail-fast: true
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v3
+
+      - name: Setup Go ${{ matrix.go }}
+        uses: actions/setup-go@v4
+        with:
+          go-version: ${{ matrix.go }}
+          cache: false
+
+      - name: Run GoSec
+        uses: securego/gosec@master
+        with:
+          args: -exclude-dir examples ./...
+
+      - name: Run GoVulnCheck
+        uses: golang/govulncheck-action@v1
+        with:
+          go-version-input: ${{ matrix.go }}
+          go-package: ./...
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..a3eb74b
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,32 @@
+name: Verify
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+permissions:
+  contents: read
+jobs:
+  lint:
+    strategy:
+      matrix:
+        go: ['1.20','1.21']
+      fail-fast: true
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v3
+
+      - name: Setup Go ${{ matrix.go }}
+        uses: actions/setup-go@v4
+        with:
+          go-version: ${{ matrix.go }}
+          cache: false
+
+      - name: Run GolangCI-Lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: v1.53
+          args: --timeout=5m
diff --git a/go.mod b/go.mod
index 7bcfa02..7c6f375 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
 module github.com/gorilla/mux
 
-go 1.19
+go 1.20
diff --git a/mux.go b/mux.go
index 1e08990..cf4672c 100644
--- a/mux.go
+++ b/mux.go
@@ -9,6 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
 	"path"
 	"regexp"
 )
@@ -19,6 +20,10 @@ var (
 	ErrMethodMismatch = errors.New("method is not allowed")
 	// ErrNotFound is returned when no route match is found.
 	ErrNotFound = errors.New("no matching route was found")
+	// RegexpCompileFunc aliases regexp.Compile and enables overriding it.
+	// Do not run this function from `init()` in importable packages.
+	// Changing this value is not safe for concurrent use.
+	RegexpCompileFunc = regexp.Compile
 )
 
 // NewRouter returns a new router instance.
@@ -84,6 +89,9 @@ type routeConf struct {
 	// will not redirect
 	skipClean bool
 
+	// If true, the http.Request context will not contain the Route.
+	omitRouteFromContext bool
+
 	// Manager for the variables from host and path.
 	regexp routeRegexpGroup
 
@@ -180,15 +188,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 		}
 		// Clean path to canonical form and redirect.
 		if p := cleanPath(path); p != path {
-
-			// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
-			// This matches with fix in go 1.2 r.c. 4 for same problem.  Go Issue:
-			// http://code.google.com/p/go/issues/detail?id=5252
-			url := *req.URL
-			url.Path = p
-			p = url.String()
-
-			w.Header().Set("Location", p)
+			w.Header().Set("Location", replaceURLPath(req.URL, p))
 			w.WriteHeader(http.StatusMovedPermanently)
 			return
 		}
@@ -197,8 +197,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	var handler http.Handler
 	if r.Match(req, &match) {
 		handler = match.Handler
-		req = requestWithVars(req, match.Vars)
-		req = requestWithRoute(req, match.Route)
+		if handler != nil {
+			// Populate context for custom handlers
+			if r.omitRouteFromContext {
+				// Only populate the match vars (if any) into the context.
+				req = requestWithVars(req, match.Vars)
+			} else {
+				req = requestWithRouteAndVars(req, match.Route, match.Vars)
+			}
+		}
 	}
 
 	if handler == nil && match.MatchErr == ErrMethodMismatch {
@@ -233,8 +240,8 @@ func (r *Router) GetRoute(name string) *Route {
 // When false, if the route path is "/path", accessing "/path/" will not match
 // this route and vice versa.
 //
-// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for
-// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed
+// The redirect is a HTTP 301 (Moved Permanently). Note that when this is set for
+// routes with a non-idempotent method (e.g. POST, PUT), the subsequent redirected
 // request will be made as a GET by most clients. Use middleware or client settings
 // to modify this behaviour as needed.
 //
@@ -260,6 +267,16 @@ func (r *Router) SkipClean(value bool) *Router {
 	return r
 }
 
+// OmitRouteFromContext defines the behavior of omitting the Route from the
+//
+//	http.Request context.
+//
+// CurrentRoute will yield nil with this option.
+func (r *Router) OmitRouteFromContext(value bool) *Router {
+	r.omitRouteFromContext = value
+	return r
+}
+
 // UseEncodedPath tells the router to match the encoded original path
 // to the routes.
 // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
@@ -445,13 +462,25 @@ func CurrentRoute(r *http.Request) *Route {
 	return nil
 }
 
+// requestWithRouteAndVars adds the matched vars to the request ctx.
+// It shortcuts the operation when the vars are empty.
 func requestWithVars(r *http.Request, vars map[string]string) *http.Request {
+	if len(vars) == 0 {
+		return r
+	}
 	ctx := context.WithValue(r.Context(), varsKey, vars)
 	return r.WithContext(ctx)
 }
 
-func requestWithRoute(r *http.Request, route *Route) *http.Request {
+// requestWithRouteAndVars adds the matched route and vars to the request ctx.
+// It saves extra allocations in cloning the request once and skipping the
+//
+//	population of empty vars, which in turn mux.Vars can handle gracefully.
+func requestWithRouteAndVars(r *http.Request, route *Route, vars map[string]string) *http.Request {
 	ctx := context.WithValue(r.Context(), routeKey, route)
+	if len(vars) > 0 {
+		ctx = context.WithValue(ctx, varsKey, vars)
+	}
 	return r.WithContext(ctx)
 }
 
@@ -478,6 +507,14 @@ func cleanPath(p string) string {
 	return np
 }
 
+// replaceURLPath prints an url.URL with a different path.
+func replaceURLPath(u *url.URL, p string) string {
+	// Operate on a copy of the request url.
+	u2 := *u
+	u2.Path = p
+	return u2.String()
+}
+
 // uniqueVars returns an error if two slices contain duplicated strings.
 func uniqueVars(s1, s2 []string) error {
 	for _, v1 := range s1 {
@@ -524,7 +561,7 @@ func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
 	}
 	m := make(map[string]*regexp.Regexp, length/2)
 	for i := 0; i < length; i += 2 {
-		regex, err := regexp.Compile(pairs[i+1])
+		regex, err := RegexpCompileFunc(pairs[i+1])
 		if err != nil {
 			return nil, err
 		}
diff --git a/regexp.go b/regexp.go
index 5d05cfa..8a1ed86 100644
--- a/regexp.go
+++ b/regexp.go
@@ -93,7 +93,7 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro
 
 		// Append variable name and compiled pattern.
 		varsN[i/2] = name
-		varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
+		varsR[i/2], err = RegexpCompileFunc(fmt.Sprintf("^%s$", patt))
 		if err != nil {
 			return nil, err
 		}
@@ -125,7 +125,7 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro
 		reverse.WriteByte('/')
 	}
 	// Compile full regexp.
-	reg, errCompile := regexp.Compile(pattern.String())
+	reg, errCompile := RegexpCompileFunc(pattern.String())
 	if errCompile != nil {
 		return nil, errCompile
 	}
@@ -324,16 +324,18 @@ type routeRegexpGroup struct {
 func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
 	// Store host variables.
 	if v.host != nil {
-		host := getHost(req)
-		if v.host.wildcardHostPort {
-			// Don't be strict on the port match
-			if i := strings.Index(host, ":"); i != -1 {
-				host = host[:i]
+		if len(v.host.varsN) > 0 {
+			host := getHost(req)
+			if v.host.wildcardHostPort {
+				// Don't be strict on the port match
+				if i := strings.Index(host, ":"); i != -1 {
+					host = host[:i]
+				}
+			}
+			matches := v.host.regexp.FindStringSubmatchIndex(host)
+			if len(matches) > 0 {
+				m.Vars = extractVars(host, matches, v.host.varsN, m.Vars)
 			}
-		}
-		matches := v.host.regexp.FindStringSubmatchIndex(host)
-		if len(matches) > 0 {
-			extractVars(host, matches, v.host.varsN, m.Vars)
 		}
 	}
 	path := req.URL.Path
@@ -342,31 +344,36 @@ func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
 	}
 	// Store path variables.
 	if v.path != nil {
-		matches := v.path.regexp.FindStringSubmatchIndex(path)
-		if len(matches) > 0 {
-			extractVars(path, matches, v.path.varsN, m.Vars)
-			// Check if we should redirect.
-			if v.path.options.strictSlash {
-				p1 := strings.HasSuffix(path, "/")
-				p2 := strings.HasSuffix(v.path.template, "/")
-				if p1 != p2 {
-					u, _ := url.Parse(req.URL.String())
-					if p1 {
-						u.Path = u.Path[:len(u.Path)-1]
-					} else {
-						u.Path += "/"
-					}
-					m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently)
+		if len(v.path.varsN) > 0 {
+			matches := v.path.regexp.FindStringSubmatchIndex(path)
+			if len(matches) > 0 {
+				m.Vars = extractVars(path, matches, v.path.varsN, m.Vars)
+			}
+		}
+		// Check if we should redirect.
+		if v.path.options.strictSlash {
+			p1 := strings.HasSuffix(path, "/")
+			p2 := strings.HasSuffix(v.path.template, "/")
+			if p1 != p2 {
+				p := req.URL.Path
+				if p1 {
+					p = p[:len(p)-1]
+				} else {
+					p += "/"
 				}
+				u := replaceURLPath(req.URL, p)
+				m.Handler = http.RedirectHandler(u, http.StatusMovedPermanently)
 			}
 		}
 	}
 	// Store query string variables.
 	for _, q := range v.queries {
-		queryURL := q.getURLQuery(req)
-		matches := q.regexp.FindStringSubmatchIndex(queryURL)
-		if len(matches) > 0 {
-			extractVars(queryURL, matches, q.varsN, m.Vars)
+		if len(q.varsN) > 0 {
+			queryURL := q.getURLQuery(req)
+			matches := q.regexp.FindStringSubmatchIndex(queryURL)
+			if len(matches) > 0 {
+				m.Vars = extractVars(queryURL, matches, q.varsN, m.Vars)
+			}
 		}
 	}
 }
@@ -381,8 +388,12 @@ func getHost(r *http.Request) string {
 	return r.Host
 }
 
-func extractVars(input string, matches []int, names []string, output map[string]string) {
+func extractVars(input string, matches []int, names []string, output map[string]string) map[string]string {
 	for i, name := range names {
+		if output == nil {
+			output = make(map[string]string, len(names))
+		}
 		output[name] = input[matches[2*i+2]:matches[2*i+3]]
 	}
+	return output
 }
diff --git a/route.go b/route.go
index e8f11df..b6582da 100644
--- a/route.go
+++ b/route.go
@@ -53,6 +53,19 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
 				continue
 			}
 
+			// Multiple routes may share the same path but use different HTTP methods. For instance:
+			// Route 1: POST "/users/{id}".
+			// Route 2: GET "/users/{id}", parameters: "id": "[0-9]+".
+			//
+			// The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2",
+			// The router should return a "Not Found" error as no route fully matches this request.
+			if rr, ok := m.(*routeRegexp); ok {
+				if rr.regexpType == regexpTypeQuery {
+					matchErr = ErrNotFound
+					break
+				}
+			}
+
 			// Ignore ErrNotFound errors. These errors arise from match call
 			// to Subrouters.
 			//
@@ -66,16 +79,6 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
 
 			matchErr = nil // nolint:ineffassign
 			return false
-		} else {
-			// Multiple routes may share the same path but use different HTTP methods. For instance:
-			// Route 1: POST "/users/{id}".
-			// Route 2: GET "/users/{id}", parameters: "id": "[0-9]+".
-			//
-			// The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2",
-			// The router should return a "Not Found" error as no route fully matches this request.
-			if match.MatchErr == ErrMethodMismatch {
-				match.MatchErr = nil
-			}
 		}
 	}
 
@@ -84,7 +87,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
 		return false
 	}
 
-	if match.MatchErr == ErrMethodMismatch && r.handler != nil {
+	if match.MatchErr != nil && r.handler != nil {
 		// We found a route which matches request method, clear MatchErr
 		match.MatchErr = nil
 		// Then override the mis-matched handler
@@ -98,9 +101,6 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
 	if match.Handler == nil {
 		match.Handler = r.handler
 	}
-	if match.Vars == nil {
-		match.Vars = make(map[string]string)
-	}
 
 	// Set variables.
 	r.regexp.setMatch(req, match, r)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index af48d22..50a3946 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
-name: CI
+name: Test
 on:
   push:
     branches:
@@ -6,15 +6,13 @@ on:
   pull_request:
     branches:
       - main
-
 permissions:
   contents: read
-
 jobs:
-  verify-and-test:
+  unit:
     strategy:
       matrix:
-        go: ['1.19','1.20']
+        go: ['1.20','1.21']
         os: [ubuntu-latest, macos-latest, windows-latest]
       fail-fast: true
     runs-on: ${{ matrix.os }}
@@ -28,28 +26,10 @@ jobs:
           go-version: ${{ matrix.go }}
           cache: false
 
-      - name: Run GolangCI-Lint
-        uses: golangci/golangci-lint-action@v3
-        with:
-          version: v1.53
-          args: --timeout=5m
-
-      - name: Run GoSec
-        if: matrix.os == 'ubuntu-latest'
-        uses: securego/gosec@master
-        with:
-          args: ./...
-
-      - name: Run GoVulnCheck
-        uses: golang/govulncheck-action@v1
-        with:
-          go-version-input: ${{ matrix.go }}
-          go-package: ./...
-
       - name: Run Tests
         run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./...
 
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
-          files: ./coverage
\ No newline at end of file
+          files: ./coverage
diff --git a/bench_test.go b/bench_test.go
index 522156d..c1f86ca 100644
--- a/bench_test.go
+++ b/bench_test.go
@@ -21,6 +21,38 @@ func BenchmarkMux(b *testing.B) {
 	}
 }
 
+func BenchmarkMuxSimple(b *testing.B) {
+	router := new(Router)
+	handler := func(w http.ResponseWriter, r *http.Request) {}
+	router.HandleFunc("/status", handler)
+
+	testCases := []struct {
+		name                 string
+		omitRouteFromContext bool
+	}{
+		{
+			name:                 "default",
+			omitRouteFromContext: false,
+		},
+		{
+			name:                 "omit route from ctx",
+			omitRouteFromContext: true,
+		},
+	}
+	for _, tc := range testCases {
+		b.Run(tc.name, func(b *testing.B) {
+			router.OmitRouteFromContext(tc.omitRouteFromContext)
+
+			request, _ := http.NewRequest("GET", "/status", nil)
+			b.ReportAllocs()
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				router.ServeHTTP(nil, request)
+			}
+		})
+	}
+}
+
 func BenchmarkMuxAlternativeInRegexp(b *testing.B) {
 	router := new(Router)
 	handler := func(w http.ResponseWriter, r *http.Request) {}
diff --git a/mux_test.go b/mux_test.go
index 5898688..0845d7f 100644
--- a/mux_test.go
+++ b/mux_test.go
@@ -59,7 +59,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route match",
 			route:       new(Route).Host("aaa.bbb.ccc"),
 			request:     newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc",
 			path:        "",
 			shouldMatch: true,
@@ -68,7 +68,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route, wrong host in request URL",
 			route:       new(Route).Host("aaa.bbb.ccc"),
 			request:     newRequest("GET", "http://aaa.222.ccc/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc",
 			path:        "",
 			shouldMatch: false,
@@ -77,7 +77,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route with port, match",
 			route:       new(Route).Host("aaa.bbb.ccc:1234"),
 			request:     newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc:1234",
 			path:        "",
 			shouldMatch: true,
@@ -86,7 +86,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route with port, wrong port in request URL",
 			route:       new(Route).Host("aaa.bbb.ccc:1234"),
 			request:     newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc:1234",
 			path:        "",
 			shouldMatch: false,
@@ -95,7 +95,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route, match with host in request header",
 			route:       new(Route).Host("aaa.bbb.ccc"),
 			request:     newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc",
 			path:        "",
 			shouldMatch: true,
@@ -104,7 +104,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route, wrong host in request header",
 			route:       new(Route).Host("aaa.bbb.ccc"),
 			request:     newRequestHost("GET", "/111/222/333", "aaa.222.ccc"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc",
 			path:        "",
 			shouldMatch: false,
@@ -113,7 +113,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route with port, match with request header",
 			route:       new(Route).Host("aaa.bbb.ccc:1234"),
 			request:     newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc:1234",
 			path:        "",
 			shouldMatch: true,
@@ -122,7 +122,7 @@ func TestHost(t *testing.T) {
 			title:       "Host route with port, wrong host in request header",
 			route:       new(Route).Host("aaa.bbb.ccc:1234"),
 			request:     newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "aaa.bbb.ccc:1234",
 			path:        "",
 			shouldMatch: false,
@@ -232,7 +232,7 @@ func TestPath(t *testing.T) {
 			title:       "Path route, match",
 			route:       new(Route).Path("/111/222/333"),
 			request:     newRequest("GET", "http://localhost/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/111/222/333",
 			shouldMatch: true,
@@ -241,7 +241,7 @@ func TestPath(t *testing.T) {
 			title:       "Path route, match with trailing slash in request and path",
 			route:       new(Route).Path("/111/"),
 			request:     newRequest("GET", "http://localhost/111/"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/111/",
 			shouldMatch: true,
@@ -250,7 +250,7 @@ func TestPath(t *testing.T) {
 			title:        "Path route, do not match with trailing slash in path",
 			route:        new(Route).Path("/111/"),
 			request:      newRequest("GET", "http://localhost/111"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/111",
 			pathTemplate: `/111/`,
@@ -261,7 +261,7 @@ func TestPath(t *testing.T) {
 			title:        "Path route, do not match with trailing slash in request",
 			route:        new(Route).Path("/111"),
 			request:      newRequest("GET", "http://localhost/111/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/111/",
 			pathTemplate: `/111`,
@@ -271,7 +271,7 @@ func TestPath(t *testing.T) {
 			title:        "Path route, match root with no host",
 			route:        new(Route).Path("/"),
 			request:      newRequest("GET", "/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -286,7 +286,7 @@ func TestPath(t *testing.T) {
 				r.RequestURI = "/"
 				return r
 			}(),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -296,7 +296,7 @@ func TestPath(t *testing.T) {
 			title:       "Path route, wrong path in request in request URL",
 			route:       new(Route).Path("/111/222/333"),
 			request:     newRequest("GET", "http://localhost/1/2/3"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/111/222/333",
 			shouldMatch: false,
@@ -468,7 +468,7 @@ func TestPathPrefix(t *testing.T) {
 			title:       "PathPrefix route, match",
 			route:       new(Route).PathPrefix("/111"),
 			request:     newRequest("GET", "http://localhost/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/111",
 			shouldMatch: true,
@@ -477,7 +477,7 @@ func TestPathPrefix(t *testing.T) {
 			title:       "PathPrefix route, match substring",
 			route:       new(Route).PathPrefix("/1"),
 			request:     newRequest("GET", "http://localhost/111/222/333"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/1",
 			shouldMatch: true,
@@ -486,7 +486,7 @@ func TestPathPrefix(t *testing.T) {
 			title:       "PathPrefix route, URL prefix in request does not match",
 			route:       new(Route).PathPrefix("/111"),
 			request:     newRequest("GET", "http://localhost/1/2/3"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "/111",
 			shouldMatch: false,
@@ -548,7 +548,7 @@ func TestSchemeHostPath(t *testing.T) {
 			title:        "Host and Path route, match",
 			route:        new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
 			request:      newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
-			vars:         map[string]string{},
+			vars:         nil,
 			scheme:       "http",
 			host:         "aaa.bbb.ccc",
 			path:         "/111/222/333",
@@ -560,7 +560,7 @@ func TestSchemeHostPath(t *testing.T) {
 			title:        "Scheme, Host, and Path route, match",
 			route:        new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"),
 			request:      newRequest("GET", "https://aaa.bbb.ccc/111/222/333"),
-			vars:         map[string]string{},
+			vars:         nil,
 			scheme:       "https",
 			host:         "aaa.bbb.ccc",
 			path:         "/111/222/333",
@@ -572,7 +572,7 @@ func TestSchemeHostPath(t *testing.T) {
 			title:        "Host and Path route, wrong host in request URL",
 			route:        new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
 			request:      newRequest("GET", "http://aaa.222.ccc/111/222/333"),
-			vars:         map[string]string{},
+			vars:         nil,
 			scheme:       "http",
 			host:         "aaa.bbb.ccc",
 			path:         "/111/222/333",
@@ -669,7 +669,7 @@ func TestHeaders(t *testing.T) {
 			title:       "Headers route, match",
 			route:       new(Route).Headers("foo", "bar", "baz", "ding"),
 			request:     newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: true,
@@ -678,7 +678,7 @@ func TestHeaders(t *testing.T) {
 			title:       "Headers route, bad header values",
 			route:       new(Route).Headers("foo", "bar", "baz", "ding"),
 			request:     newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: false,
@@ -687,7 +687,7 @@ func TestHeaders(t *testing.T) {
 			title:       "Headers route, regex header values to match",
 			route:       new(Route).HeadersRegexp("foo", "ba[zr]"),
 			request:     newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baw"}),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: false,
@@ -696,7 +696,7 @@ func TestHeaders(t *testing.T) {
 			title:       "Headers route, regex header values to match",
 			route:       new(Route).HeadersRegexp("foo", "ba[zr]"),
 			request:     newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: true,
@@ -717,7 +717,7 @@ func TestMethods(t *testing.T) {
 			title:       "Methods route, match GET",
 			route:       new(Route).Methods("GET", "POST"),
 			request:     newRequest("GET", "http://localhost"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			methods:     []string{"GET", "POST"},
@@ -727,7 +727,7 @@ func TestMethods(t *testing.T) {
 			title:       "Methods route, match POST",
 			route:       new(Route).Methods("GET", "POST"),
 			request:     newRequest("POST", "http://localhost"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			methods:     []string{"GET", "POST"},
@@ -737,7 +737,7 @@ func TestMethods(t *testing.T) {
 			title:       "Methods route, bad method",
 			route:       new(Route).Methods("GET", "POST"),
 			request:     newRequest("PUT", "http://localhost"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			methods:     []string{"GET", "POST"},
@@ -747,7 +747,7 @@ func TestMethods(t *testing.T) {
 			title:       "Route without methods",
 			route:       new(Route),
 			request:     newRequest("PUT", "http://localhost"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			methods:     []string{},
@@ -770,7 +770,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route, match",
 			route:           new(Route).Queries("foo", "bar", "baz", "ding"),
 			request:         newRequest("GET", "http://localhost?foo=bar&baz=ding"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			query:           "foo=bar&baz=ding",
@@ -782,7 +782,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route, match with a query string",
 			route:           new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
 			request:         newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			query:           "foo=bar&baz=ding",
@@ -796,7 +796,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route, match with a query string out of order",
 			route:           new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
 			request:         newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			query:           "foo=bar&baz=ding",
@@ -810,7 +810,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route, bad query",
 			route:           new(Route).Queries("foo", "bar", "baz", "ding"),
 			request:         newRequest("GET", "http://localhost?foo=bar&baz=dong"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo=bar,baz=ding",
@@ -857,7 +857,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with regexp pattern, regexp does not match",
 			route:           new(Route).Queries("foo", "{v1:[0-9]+}"),
 			request:         newRequest("GET", "http://localhost?foo=a"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo={v1:[0-9]+}",
@@ -892,7 +892,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with regexp pattern with quantifier, regexp does not match",
 			route:           new(Route).Queries("foo", "{v1:[0-9]{1}}"),
 			request:         newRequest("GET", "http://localhost?foo=12"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo={v1:[0-9]{1}}",
@@ -915,7 +915,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with regexp pattern with quantifier, additional variable in query string, regexp does not match",
 			route:           new(Route).Queries("foo", "{v1:[0-9]{1}}"),
 			request:         newRequest("GET", "http://localhost?foo=12"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo={v1:[0-9]{1}}",
@@ -974,7 +974,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with empty value, should match",
 			route:           new(Route).Queries("foo", ""),
 			request:         newRequest("GET", "http://localhost?foo=bar"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			query:           "foo=",
@@ -986,7 +986,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with empty value and no parameter in request, should not match",
 			route:           new(Route).Queries("foo", ""),
 			request:         newRequest("GET", "http://localhost"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo=",
@@ -997,7 +997,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with empty value and empty parameter in request, should match",
 			route:           new(Route).Queries("foo", ""),
 			request:         newRequest("GET", "http://localhost?foo="),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			query:           "foo=",
@@ -1009,7 +1009,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with overlapping value, should not match",
 			route:           new(Route).Queries("foo", "bar"),
 			request:         newRequest("GET", "http://localhost?foo=barfoo"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo=bar",
@@ -1020,7 +1020,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route with no parameter in request, should not match",
 			route:           new(Route).Queries("foo", "{bar}"),
 			request:         newRequest("GET", "http://localhost"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo={bar}",
@@ -1043,7 +1043,7 @@ func TestQueries(t *testing.T) {
 			title:           "Queries route, bad submatch",
 			route:           new(Route).Queries("foo", "bar", "baz", "ding"),
 			request:         newRequest("GET", "http://localhost?fffoo=bar&baz=dingggg"),
-			vars:            map[string]string{},
+			vars:            nil,
 			host:            "",
 			path:            "",
 			queriesTemplate: "foo=bar,baz=ding",
@@ -1136,7 +1136,7 @@ func TestMatcherFunc(t *testing.T) {
 			title:       "MatchFunc route, match",
 			route:       new(Route).MatcherFunc(m),
 			request:     newRequest("GET", "http://aaa.bbb.ccc"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: true,
@@ -1145,7 +1145,7 @@ func TestMatcherFunc(t *testing.T) {
 			title:       "MatchFunc route, non-match",
 			route:       new(Route).MatcherFunc(m),
 			request:     newRequest("GET", "http://aaa.222.ccc"),
-			vars:        map[string]string{},
+			vars:        nil,
 			host:        "",
 			path:        "",
 			shouldMatch: false,
@@ -1246,7 +1246,7 @@ func TestSubRouter(t *testing.T) {
 		{
 			route:        subrouter3.Path("/"),
 			request:      newRequest("GET", "http://localhost/foo/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo/",
 			pathTemplate: `/foo/`,
@@ -1255,7 +1255,7 @@ func TestSubRouter(t *testing.T) {
 		{
 			route:        subrouter3.Path(""),
 			request:      newRequest("GET", "http://localhost/foo"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo",
 			pathTemplate: `/foo`,
@@ -1265,7 +1265,7 @@ func TestSubRouter(t *testing.T) {
 		{
 			route:        subrouter4.Path("/"),
 			request:      newRequest("GET", "http://localhost/foo/bar/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo/bar/",
 			pathTemplate: `/foo/bar/`,
@@ -1274,7 +1274,7 @@ func TestSubRouter(t *testing.T) {
 		{
 			route:        subrouter4.Path(""),
 			request:      newRequest("GET", "http://localhost/foo/bar"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo/bar",
 			pathTemplate: `/foo/bar`,
@@ -1302,7 +1302,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Mismatch method specified on parent route",
 			route:        new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"),
 			request:      newRequest("GET", "http://localhost/foo/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo/",
 			pathTemplate: `/foo/`,
@@ -1312,7 +1312,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Match method specified on parent route",
 			route:        new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"),
 			request:      newRequest("POST", "http://localhost/foo/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/foo/",
 			pathTemplate: `/foo/`,
@@ -1322,7 +1322,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Mismatch scheme specified on parent route",
 			route:        new(Route).Schemes("https").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1332,7 +1332,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Match scheme specified on parent route",
 			route:        new(Route).Schemes("http").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1342,7 +1342,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "No match header specified on parent route",
 			route:        new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1352,7 +1352,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Header mismatch value specified on parent route",
 			route:        new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"),
 			request:      newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "http"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1362,7 +1362,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Header match value specified on parent route",
 			route:        new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"),
 			request:      newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "https"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1372,7 +1372,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Query specified on parent route not present",
 			route:        new(Route).Headers("key", "foobar").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1382,7 +1382,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Query mismatch value specified on parent route",
 			route:        new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/?key=notfoobar"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1392,7 +1392,7 @@ func TestSubRouter(t *testing.T) {
 			title:        "Query match value specified on subroute",
 			route:        new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"),
 			request:      newRequest("GET", "http://localhost/?key=foobar"),
-			vars:         map[string]string{},
+			vars:         nil,
 			host:         "",
 			path:         "/",
 			pathTemplate: `/`,
@@ -1473,7 +1473,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Redirect path without slash",
 			route:          r.NewRoute().Path("/111/"),
 			request:        newRequest("GET", "http://localhost/111"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/111/",
 			shouldMatch:    true,
@@ -1483,7 +1483,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Do not redirect path with slash",
 			route:          r.NewRoute().Path("/111/"),
 			request:        newRequest("GET", "http://localhost/111/"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/111/",
 			shouldMatch:    true,
@@ -1493,7 +1493,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Redirect path with slash",
 			route:          r.NewRoute().Path("/111"),
 			request:        newRequest("GET", "http://localhost/111/"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/111",
 			shouldMatch:    true,
@@ -1503,7 +1503,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Do not redirect path without slash",
 			route:          r.NewRoute().Path("/111"),
 			request:        newRequest("GET", "http://localhost/111"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/111",
 			shouldMatch:    true,
@@ -1513,7 +1513,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Propagate StrictSlash to subrouters",
 			route:          r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"),
 			request:        newRequest("GET", "http://localhost/static/images"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/static/images/",
 			shouldMatch:    true,
@@ -1523,7 +1523,7 @@ func TestStrictSlash(t *testing.T) {
 			title:          "Ignore StrictSlash for path prefix",
 			route:          r.NewRoute().PathPrefix("/static/"),
 			request:        newRequest("GET", "http://localhost/static/logo.png"),
-			vars:           map[string]string{},
+			vars:           nil,
 			host:           "",
 			path:           "/static/",
 			shouldMatch:    true,
@@ -2116,6 +2116,24 @@ func TestMultipleDefinitionOfSamePathWithDifferentMethods(t *testing.T) {
 
 }
 
+func TestMultipleDefinitionOfSamePathWithDifferentQueries(t *testing.T) {
+	emptyHandler := func(w http.ResponseWriter, r *http.Request) {}
+
+	r := NewRouter()
+	r.HandleFunc("/api", emptyHandler).Queries("foo", "{foo:[0-9]+}").Methods(http.MethodGet)
+	r.HandleFunc("/api", emptyHandler).Queries("bar", "{bar:[0-9]+}").Methods(http.MethodGet)
+
+	req := newRequest(http.MethodGet, "/api?bar=4")
+	match := new(RouteMatch)
+	matched := r.Match(req, match)
+	if !matched {
+		t.Error("Should have matched route for methods")
+	}
+	if match.MatchErr != nil {
+		t.Error("Should have no error. Found:", match.MatchErr)
+	}
+}
+
 func TestErrMatchNotFound(t *testing.T) {
 	emptyHandler := func(w http.ResponseWriter, r *http.Request) {}
 
@@ -2784,6 +2802,23 @@ func TestMethodNotAllowed(t *testing.T) {
 	}
 }
 
+func TestMethodNotAllowedSubrouterWithSeveralRoutes(t *testing.T) {
+	handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }
+
+	router := NewRouter()
+	subrouter := router.PathPrefix("/v1").Subrouter()
+	subrouter.HandleFunc("/api", handler).Methods(http.MethodGet)
+	subrouter.HandleFunc("/api/{id}", handler).Methods(http.MethodGet)
+
+	w := NewRecorder()
+	req := newRequest(http.MethodPut, "/v1/api")
+	router.ServeHTTP(w, req)
+
+	if w.Code != http.StatusMethodNotAllowed {
+		t.Errorf("Expected status code 405 (got %d)", w.Code)
+	}
+}
+
 type customMethodNotAllowedHandler struct {
 	msg string
 }
@@ -2913,6 +2948,128 @@ func TestGetVarNames(t *testing.T) {
 	}
 }
 
+func getPopulateContextTestCases() []struct {
+	name                 string
+	path                 string
+	omitRouteFromContext bool
+	wantVar              string
+	wantStaticRoute      bool
+	wantDynamicRoute     bool
+} {
+	return []struct {
+		name                 string
+		path                 string
+		omitRouteFromContext bool
+		wantVar              string
+		wantStaticRoute      bool
+		wantDynamicRoute     bool
+	}{
+		{
+			name:            "no populated vars",
+			path:            "/static",
+			wantVar:         "",
+			wantStaticRoute: true,
+		},
+		{
+			name:             "empty var",
+			path:             "/dynamic/",
+			wantVar:          "",
+			wantDynamicRoute: true,
+		},
+		{
+			name:             "populated vars",
+			path:             "/dynamic/foo",
+			wantVar:          "foo",
+			wantDynamicRoute: true,
+		},
+		{
+			name:                 "omit route /static",
+			path:                 "/static",
+			omitRouteFromContext: true,
+			wantVar:              "",
+			wantStaticRoute:      false,
+		},
+		{
+			name:                 "omit route /dynamic",
+			path:                 "/dynamic/",
+			omitRouteFromContext: true,
+			wantVar:              "",
+			wantDynamicRoute:     false,
+		},
+	}
+}
+
+func TestPopulateContext(t *testing.T) {
+	testCases := getPopulateContextTestCases()
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			matched := false
+			r := NewRouter()
+			r.OmitRouteFromContext(tc.omitRouteFromContext)
+			var static *Route
+			var dynamic *Route
+			fn := func(w http.ResponseWriter, r *http.Request) {
+				matched = true
+				if got := Vars(r)["x"]; got != tc.wantVar {
+					t.Fatalf("wantVar=%q, got=%q", tc.wantVar, got)
+				}
+				switch {
+				case tc.wantDynamicRoute:
+					r2 := CurrentRoute(r)
+					if r2 != dynamic || r2.GetName() != "dynamic" {
+						t.Fatalf("expected dynmic route in ctx, got %v", r2)
+					}
+				case tc.wantStaticRoute:
+					r2 := CurrentRoute(r)
+					if r2 != static || r2.GetName() != "static" {
+						t.Fatalf("expected static route in ctx, got %v", r2)
+					}
+				default:
+					if r2 := CurrentRoute(r); r2 != nil {
+						t.Fatalf("expected no route in ctx, got %v", r2)
+					}
+				}
+				w.WriteHeader(http.StatusNoContent)
+			}
+			static = r.Name("static").Path("/static").HandlerFunc(fn)
+			dynamic = r.Name("dynamic").Path("/dynamic/{x:.*}").HandlerFunc(fn)
+			req := newRequest(http.MethodGet, "http://localhost"+tc.path)
+			rec := NewRecorder()
+			r.ServeHTTP(rec, req)
+			if !matched {
+				t.Fatal("Expected route to match")
+			}
+		})
+	}
+}
+
+func BenchmarkPopulateContext(b *testing.B) {
+	testCases := getPopulateContextTestCases()
+	for _, tc := range testCases {
+		b.Run(tc.name, func(b *testing.B) {
+			matched := false
+			r := NewRouter()
+			r.OmitRouteFromContext(tc.omitRouteFromContext)
+			fn := func(w http.ResponseWriter, r *http.Request) {
+				matched = true
+				w.WriteHeader(http.StatusNoContent)
+			}
+			r.Name("static").Path("/static").HandlerFunc(fn)
+			r.Name("dynamic").Path("/dynamic/{x:.*}").HandlerFunc(fn)
+			req := newRequest(http.MethodGet, "http://localhost"+tc.path)
+			rec := NewRecorder()
+			b.ReportAllocs()
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				r.ServeHTTP(rec, req)
+			}
+			if !matched {
+				b.Fatal("Expected route to match")
+			}
+		})
+	}
+}
+
 // mapToPairs converts a string map to a slice of string pairs
 func mapToPairs(m map[string]string) []string {
 	var i int
diff --git a/route_test.go b/route_test.go
new file mode 100644
index 0000000..5aa35d1
--- /dev/null
+++ b/route_test.go
@@ -0,0 +1,66 @@
+package mux
+
+import (
+	"net/http"
+	"regexp"
+	"sync"
+	"testing"
+)
+
+var testNewRouterMu sync.Mutex
+var testHandler = http.NotFoundHandler()
+
+func BenchmarkNewRouter(b *testing.B) {
+	testNewRouterMu.Lock()
+	defer testNewRouterMu.Unlock()
+
+	// Set the RegexpCompileFunc to the default regexp.Compile.
+	RegexpCompileFunc = regexp.Compile
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		testNewRouter(b, testHandler)
+	}
+}
+
+func BenchmarkNewRouterRegexpFunc(b *testing.B) {
+	testNewRouterMu.Lock()
+	defer testNewRouterMu.Unlock()
+
+	// We preallocate the size to 8.
+	cache := make(map[string]*regexp.Regexp, 8)
+
+	// Override the RegexpCompileFunc to reuse compiled expressions
+	// from the `cache` map. Real world caches should have eviction
+	// policies or some sort of approach to limit memory use.
+	RegexpCompileFunc = func(expr string) (*regexp.Regexp, error) {
+		if regex, ok := cache[expr]; ok {
+			return regex, nil
+		}
+
+		regex, err := regexp.Compile(expr)
+		if err != nil {
+			return nil, err
+		}
+
+		cache[expr] = regex
+		return regex, nil
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		testNewRouter(b, testHandler)
+	}
+}
+
+func testNewRouter(_ testing.TB, handler http.Handler) {
+	r := NewRouter()
+	// A route with a route variable:
+	r.Handle("/metrics/{type}", handler)
+	r.Queries("orgID", "{orgID:[0-9]*?}")
+	r.Host("{subdomain}.domain.com")
+}