├── .editorconfig ├── .github └── workflows │ ├── issues.yml │ ├── security.yml │ ├── test.yml │ └── verify.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── doc.go ├── example_authentication_middleware_test.go ├── example_cors_method_middleware_test.go ├── example_route_test.go ├── example_route_vars_test.go ├── go.mod ├── middleware.go ├── middleware_test.go ├── mux.go ├── mux_httpserver_test.go ├── mux_test.go ├── old_test.go ├── regexp.go ├── regexp_test.go ├── route.go ├── route_test.go └── test_helpers.go /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_size = 4 18 | trim_trailing_whitespace = false 19 | 20 | eclint_indent_style = unset -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | # Add all the issues created to the project. 2 | name: Add issue or pull request to Project 3 | 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Add issue to project 18 | uses: actions/add-to-project@v0.5.0 19 | with: 20 | project-url: https://github.com/orgs/gorilla/projects/4 21 | github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | scan: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GoSec 29 | uses: securego/gosec@master 30 | with: 31 | args: -exclude-dir examples ./... 32 | 33 | - name: Run GoVulnCheck 34 | uses: golang/govulncheck-action@v1 35 | with: 36 | go-version-input: ${{ matrix.go }} 37 | go-package: ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | unit: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | fail-fast: true 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Go ${{ matrix.go }} 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: ${{ matrix.go }} 27 | cache: false 28 | 29 | - name: Run Tests 30 | run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./... 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v3 34 | with: 35 | files: ./coverage 36 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | lint: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GolangCI-Lint 29 | uses: golangci/golangci-lint-action@v3 30 | with: 31 | version: v1.53 32 | args: --timeout=5m 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.coverprofile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 The Gorilla Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') 2 | GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest 3 | 4 | GO_SEC=$(shell which gosec 2> /dev/null || echo '') 5 | GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest 6 | 7 | GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') 8 | GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest 9 | 10 | .PHONY: golangci-lint 11 | golangci-lint: 12 | $(if $(GO_LINT), ,go install $(GO_LINT_URI)) 13 | @echo "##### Running golangci-lint" 14 | golangci-lint run -v 15 | 16 | .PHONY: gosec 17 | gosec: 18 | $(if $(GO_SEC), ,go install $(GO_SEC_URI)) 19 | @echo "##### Running gosec" 20 | gosec ./... 21 | 22 | .PHONY: govulncheck 23 | govulncheck: 24 | $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) 25 | @echo "##### Running govulncheck" 26 | govulncheck ./... 27 | 28 | .PHONY: verify 29 | verify: golangci-lint gosec govulncheck 30 | 31 | .PHONY: test 32 | test: 33 | @echo "##### Running tests" 34 | go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gorilla/mux 2 | 3 | ![testing](https://github.com/gorilla/mux/actions/workflows/test.yml/badge.svg) 4 | [![codecov](https://codecov.io/github/gorilla/mux/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/mux) 5 | [![godoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) 6 | [![sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) 7 | 8 | 9 | ![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 10 | 11 | Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to 12 | their respective handler. 13 | 14 | The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: 15 | 16 | * It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. 17 | * Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. 18 | * URL hosts, paths and query values can have variables with an optional regular expression. 19 | * Registered URLs can be built, or "reversed", which helps maintaining references to resources. 20 | * Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. 21 | 22 | --- 23 | 24 | * [Install](#install) 25 | * [Examples](#examples) 26 | * [Matching Routes](#matching-routes) 27 | * [Static Files](#static-files) 28 | * [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) 29 | * [Registered URLs](#registered-urls) 30 | * [Walking Routes](#walking-routes) 31 | * [Graceful Shutdown](#graceful-shutdown) 32 | * [Middleware](#middleware) 33 | * [Handling CORS Requests](#handling-cors-requests) 34 | * [Testing Handlers](#testing-handlers) 35 | * [Full Example](#full-example) 36 | 37 | --- 38 | 39 | ## Install 40 | 41 | With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: 42 | 43 | ```sh 44 | go get -u github.com/gorilla/mux 45 | ``` 46 | 47 | ## Examples 48 | 49 | Let's start registering a couple of URL paths and handlers: 50 | 51 | ```go 52 | func main() { 53 | r := mux.NewRouter() 54 | r.HandleFunc("/", HomeHandler) 55 | r.HandleFunc("/products", ProductsHandler) 56 | r.HandleFunc("/articles", ArticlesHandler) 57 | http.Handle("/", r) 58 | } 59 | ``` 60 | 61 | Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. 62 | 63 | Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: 64 | 65 | ```go 66 | r := mux.NewRouter() 67 | r.HandleFunc("/products/{key}", ProductHandler) 68 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 69 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 70 | ``` 71 | 72 | The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: 73 | 74 | ```go 75 | func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { 76 | vars := mux.Vars(r) 77 | w.WriteHeader(http.StatusOK) 78 | fmt.Fprintf(w, "Category: %v\n", vars["category"]) 79 | } 80 | ``` 81 | 82 | And this is all you need to know about the basic usage. More advanced options are explained below. 83 | 84 | ### Matching Routes 85 | 86 | Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: 87 | 88 | ```go 89 | r := mux.NewRouter() 90 | // Only matches if domain is "www.example.com". 91 | r.Host("www.example.com") 92 | // Matches a dynamic subdomain. 93 | r.Host("{subdomain:[a-z]+}.example.com") 94 | ``` 95 | 96 | There are several other matchers that can be added. To match path prefixes: 97 | 98 | ```go 99 | r.PathPrefix("/products/") 100 | ``` 101 | 102 | ...or HTTP methods: 103 | 104 | ```go 105 | r.Methods("GET", "POST") 106 | ``` 107 | 108 | ...or URL schemes: 109 | 110 | ```go 111 | r.Schemes("https") 112 | ``` 113 | 114 | ...or header values: 115 | 116 | ```go 117 | r.Headers("X-Requested-With", "XMLHttpRequest") 118 | ``` 119 | 120 | ...or query values: 121 | 122 | ```go 123 | r.Queries("key", "value") 124 | ``` 125 | 126 | ...or to use a custom matcher function: 127 | 128 | ```go 129 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 130 | return r.ProtoMajor == 0 131 | }) 132 | ``` 133 | 134 | ...and finally, it is possible to combine several matchers in a single route: 135 | 136 | ```go 137 | r.HandleFunc("/products", ProductsHandler). 138 | Host("www.example.com"). 139 | Methods("GET"). 140 | Schemes("http") 141 | ``` 142 | 143 | Routes are tested in the order they were added to the router. If two routes match, the first one wins: 144 | 145 | ```go 146 | r := mux.NewRouter() 147 | r.HandleFunc("/specific", specificHandler) 148 | r.PathPrefix("/").Handler(catchAllHandler) 149 | ``` 150 | 151 | Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". 152 | 153 | For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: 154 | 155 | ```go 156 | r := mux.NewRouter() 157 | s := r.Host("www.example.com").Subrouter() 158 | ``` 159 | 160 | Then register routes in the subrouter: 161 | 162 | ```go 163 | s.HandleFunc("/products/", ProductsHandler) 164 | s.HandleFunc("/products/{key}", ProductHandler) 165 | s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 166 | ``` 167 | 168 | The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. 169 | 170 | Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. 171 | 172 | There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: 173 | 174 | ```go 175 | r := mux.NewRouter() 176 | s := r.PathPrefix("/products").Subrouter() 177 | // "/products/" 178 | s.HandleFunc("/", ProductsHandler) 179 | // "/products/{key}/" 180 | s.HandleFunc("/{key}/", ProductHandler) 181 | // "/products/{key}/details" 182 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 183 | ``` 184 | 185 | 186 | ### Static Files 187 | 188 | Note that the path provided to `PathPrefix()` represents a "wildcard": calling 189 | `PathPrefix("/static/").Handler(...)` means that the handler will be passed any 190 | request that matches "/static/\*". This makes it easy to serve static files with mux: 191 | 192 | ```go 193 | func main() { 194 | var dir string 195 | 196 | flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") 197 | flag.Parse() 198 | r := mux.NewRouter() 199 | 200 | // This will serve files under http://localhost:8000/static/ 201 | r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) 202 | 203 | srv := &http.Server{ 204 | Handler: r, 205 | Addr: "127.0.0.1:8000", 206 | // Good practice: enforce timeouts for servers you create! 207 | WriteTimeout: 15 * time.Second, 208 | ReadTimeout: 15 * time.Second, 209 | } 210 | 211 | log.Fatal(srv.ListenAndServe()) 212 | } 213 | ``` 214 | 215 | ### Serving Single Page Applications 216 | 217 | Most of the time it makes sense to serve your SPA on a separate web server from your API, 218 | but sometimes it's desirable to serve them both from one place. It's possible to write a simple 219 | handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage 220 | mux's powerful routing for your API endpoints. 221 | 222 | ```go 223 | package main 224 | 225 | import ( 226 | "encoding/json" 227 | "log" 228 | "net/http" 229 | "os" 230 | "path/filepath" 231 | "time" 232 | 233 | "github.com/gorilla/mux" 234 | ) 235 | 236 | // spaHandler implements the http.Handler interface, so we can use it 237 | // to respond to HTTP requests. The path to the static directory and 238 | // path to the index file within that static directory are used to 239 | // serve the SPA in the given static directory. 240 | type spaHandler struct { 241 | staticPath string 242 | indexPath string 243 | } 244 | 245 | // ServeHTTP inspects the URL path to locate a file within the static dir 246 | // on the SPA handler. If a file is found, it will be served. If not, the 247 | // file located at the index path on the SPA handler will be served. This 248 | // is suitable behavior for serving an SPA (single page application). 249 | func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 250 | // Join internally call path.Clean to prevent directory traversal 251 | path := filepath.Join(h.staticPath, r.URL.Path) 252 | 253 | // check whether a file exists or is a directory at the given path 254 | fi, err := os.Stat(path) 255 | if os.IsNotExist(err) || fi.IsDir() { 256 | // file does not exist or path is a directory, serve index.html 257 | http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) 258 | return 259 | } 260 | 261 | if err != nil { 262 | // if we got an error (that wasn't that the file doesn't exist) stating the 263 | // file, return a 500 internal server error and stop 264 | http.Error(w, err.Error(), http.StatusInternalServerError) 265 | return 266 | } 267 | 268 | // otherwise, use http.FileServer to serve the static file 269 | http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) 270 | } 271 | 272 | func main() { 273 | router := mux.NewRouter() 274 | 275 | router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { 276 | // an example API handler 277 | json.NewEncoder(w).Encode(map[string]bool{"ok": true}) 278 | }) 279 | 280 | spa := spaHandler{staticPath: "build", indexPath: "index.html"} 281 | router.PathPrefix("/").Handler(spa) 282 | 283 | srv := &http.Server{ 284 | Handler: router, 285 | Addr: "127.0.0.1:8000", 286 | // Good practice: enforce timeouts for servers you create! 287 | WriteTimeout: 15 * time.Second, 288 | ReadTimeout: 15 * time.Second, 289 | } 290 | 291 | log.Fatal(srv.ListenAndServe()) 292 | } 293 | ``` 294 | 295 | ### Registered URLs 296 | 297 | Now let's see how to build registered URLs. 298 | 299 | Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: 300 | 301 | ```go 302 | r := mux.NewRouter() 303 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 304 | Name("article") 305 | ``` 306 | 307 | To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: 308 | 309 | ```go 310 | url, err := r.Get("article").URL("category", "technology", "id", "42") 311 | ``` 312 | 313 | ...and the result will be a `url.URL` with the following path: 314 | 315 | ``` 316 | "/articles/technology/42" 317 | ``` 318 | 319 | This also works for host and query value variables: 320 | 321 | ```go 322 | r := mux.NewRouter() 323 | r.Host("{subdomain}.example.com"). 324 | Path("/articles/{category}/{id:[0-9]+}"). 325 | Queries("filter", "{filter}"). 326 | HandlerFunc(ArticleHandler). 327 | Name("article") 328 | 329 | // url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" 330 | url, err := r.Get("article").URL("subdomain", "news", 331 | "category", "technology", 332 | "id", "42", 333 | "filter", "gorilla") 334 | ``` 335 | 336 | All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. 337 | 338 | Regex support also exists for matching Headers within a route. For example, we could do: 339 | 340 | ```go 341 | r.HeadersRegexp("Content-Type", "application/(text|json)") 342 | ``` 343 | 344 | ...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` 345 | 346 | There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: 347 | 348 | ```go 349 | // "http://news.example.com/" 350 | host, err := r.Get("article").URLHost("subdomain", "news") 351 | 352 | // "/articles/technology/42" 353 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 354 | ``` 355 | 356 | And if you use subrouters, host and path defined separately can be built as well: 357 | 358 | ```go 359 | r := mux.NewRouter() 360 | s := r.Host("{subdomain}.example.com").Subrouter() 361 | s.Path("/articles/{category}/{id:[0-9]+}"). 362 | HandlerFunc(ArticleHandler). 363 | Name("article") 364 | 365 | // "http://news.example.com/articles/technology/42" 366 | url, err := r.Get("article").URL("subdomain", "news", 367 | "category", "technology", 368 | "id", "42") 369 | ``` 370 | 371 | To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available: 372 | ```go 373 | r := mux.NewRouter() 374 | r.Host("{domain}"). 375 | Path("/{group}/{item_id}"). 376 | Queries("some_data1", "{some_data1}"). 377 | Queries("some_data2", "{some_data2}"). 378 | Name("article") 379 | 380 | // Will print [domain group item_id some_data1 some_data2] 381 | fmt.Println(r.Get("article").GetVarNames()) 382 | 383 | ``` 384 | ### Walking Routes 385 | 386 | The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, 387 | the following prints all of the registered routes: 388 | 389 | ```go 390 | package main 391 | 392 | import ( 393 | "fmt" 394 | "net/http" 395 | "strings" 396 | 397 | "github.com/gorilla/mux" 398 | ) 399 | 400 | func handler(w http.ResponseWriter, r *http.Request) { 401 | return 402 | } 403 | 404 | func main() { 405 | r := mux.NewRouter() 406 | r.HandleFunc("/", handler) 407 | r.HandleFunc("/products", handler).Methods("POST") 408 | r.HandleFunc("/articles", handler).Methods("GET") 409 | r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") 410 | r.HandleFunc("/authors", handler).Queries("surname", "{surname}") 411 | err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 412 | pathTemplate, err := route.GetPathTemplate() 413 | if err == nil { 414 | fmt.Println("ROUTE:", pathTemplate) 415 | } 416 | pathRegexp, err := route.GetPathRegexp() 417 | if err == nil { 418 | fmt.Println("Path regexp:", pathRegexp) 419 | } 420 | queriesTemplates, err := route.GetQueriesTemplates() 421 | if err == nil { 422 | fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) 423 | } 424 | queriesRegexps, err := route.GetQueriesRegexp() 425 | if err == nil { 426 | fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) 427 | } 428 | methods, err := route.GetMethods() 429 | if err == nil { 430 | fmt.Println("Methods:", strings.Join(methods, ",")) 431 | } 432 | fmt.Println() 433 | return nil 434 | }) 435 | 436 | if err != nil { 437 | fmt.Println(err) 438 | } 439 | 440 | http.Handle("/", r) 441 | } 442 | ``` 443 | 444 | ### Graceful Shutdown 445 | 446 | Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: 447 | 448 | ```go 449 | package main 450 | 451 | import ( 452 | "context" 453 | "flag" 454 | "log" 455 | "net/http" 456 | "os" 457 | "os/signal" 458 | "time" 459 | 460 | "github.com/gorilla/mux" 461 | ) 462 | 463 | func main() { 464 | var wait time.Duration 465 | flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") 466 | flag.Parse() 467 | 468 | r := mux.NewRouter() 469 | // Add your routes as needed 470 | 471 | srv := &http.Server{ 472 | Addr: "0.0.0.0:8080", 473 | // Good practice to set timeouts to avoid Slowloris attacks. 474 | WriteTimeout: time.Second * 15, 475 | ReadTimeout: time.Second * 15, 476 | IdleTimeout: time.Second * 60, 477 | Handler: r, // Pass our instance of gorilla/mux in. 478 | } 479 | 480 | // Run our server in a goroutine so that it doesn't block. 481 | go func() { 482 | if err := srv.ListenAndServe(); err != nil { 483 | log.Println(err) 484 | } 485 | }() 486 | 487 | c := make(chan os.Signal, 1) 488 | // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) 489 | // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. 490 | signal.Notify(c, os.Interrupt) 491 | 492 | // Block until we receive our signal. 493 | <-c 494 | 495 | // Create a deadline to wait for. 496 | ctx, cancel := context.WithTimeout(context.Background(), wait) 497 | defer cancel() 498 | // Doesn't block if no connections, but will otherwise wait 499 | // until the timeout deadline. 500 | srv.Shutdown(ctx) 501 | // Optionally, you could run srv.Shutdown in a goroutine and block on 502 | // <-ctx.Done() if your application should wait for other services 503 | // to finalize based on context cancellation. 504 | log.Println("shutting down") 505 | os.Exit(0) 506 | } 507 | ``` 508 | 509 | ### Middleware 510 | 511 | Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. 512 | Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. 513 | 514 | Mux middlewares are defined using the de facto standard type: 515 | 516 | ```go 517 | type MiddlewareFunc func(http.Handler) http.Handler 518 | ``` 519 | 520 | Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. 521 | 522 | A very basic middleware which logs the URI of the request being handled could be written as: 523 | 524 | ```go 525 | func loggingMiddleware(next http.Handler) http.Handler { 526 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 527 | // Do stuff here 528 | log.Println(r.RequestURI) 529 | // Call the next handler, which can be another middleware in the chain, or the final handler. 530 | next.ServeHTTP(w, r) 531 | }) 532 | } 533 | ``` 534 | 535 | Middlewares can be added to a router using `Router.Use()`: 536 | 537 | ```go 538 | r := mux.NewRouter() 539 | r.HandleFunc("/", handler) 540 | r.Use(loggingMiddleware) 541 | ``` 542 | 543 | A more complex authentication middleware, which maps session token to users, could be written as: 544 | 545 | ```go 546 | // Define our struct 547 | type authenticationMiddleware struct { 548 | tokenUsers map[string]string 549 | } 550 | 551 | // Initialize it somewhere 552 | func (amw *authenticationMiddleware) Populate() { 553 | amw.tokenUsers["00000000"] = "user0" 554 | amw.tokenUsers["aaaaaaaa"] = "userA" 555 | amw.tokenUsers["05f717e5"] = "randomUser" 556 | amw.tokenUsers["deadbeef"] = "user0" 557 | } 558 | 559 | // Middleware function, which will be called for each request 560 | func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { 561 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 562 | token := r.Header.Get("X-Session-Token") 563 | 564 | if user, found := amw.tokenUsers[token]; found { 565 | // We found the token in our map 566 | log.Printf("Authenticated user %s\n", user) 567 | // Pass down the request to the next middleware (or final handler) 568 | next.ServeHTTP(w, r) 569 | } else { 570 | // Write an error and stop the handler chain 571 | http.Error(w, "Forbidden", http.StatusForbidden) 572 | } 573 | }) 574 | } 575 | ``` 576 | 577 | ```go 578 | r := mux.NewRouter() 579 | r.HandleFunc("/", handler) 580 | 581 | amw := authenticationMiddleware{tokenUsers: make(map[string]string)} 582 | amw.Populate() 583 | 584 | r.Use(amw.Middleware) 585 | ``` 586 | 587 | Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. 588 | 589 | ### Handling CORS Requests 590 | 591 | [CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. 592 | 593 | * You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` 594 | * The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route 595 | * If you do not specify any methods, then: 596 | > _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. 597 | 598 | Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: 599 | 600 | ```go 601 | package main 602 | 603 | import ( 604 | "net/http" 605 | "github.com/gorilla/mux" 606 | ) 607 | 608 | func main() { 609 | r := mux.NewRouter() 610 | 611 | // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers 612 | r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) 613 | r.Use(mux.CORSMethodMiddleware(r)) 614 | 615 | http.ListenAndServe(":8080", r) 616 | } 617 | 618 | func fooHandler(w http.ResponseWriter, r *http.Request) { 619 | w.Header().Set("Access-Control-Allow-Origin", "*") 620 | if r.Method == http.MethodOptions { 621 | return 622 | } 623 | 624 | w.Write([]byte("foo")) 625 | } 626 | ``` 627 | 628 | And an request to `/foo` using something like: 629 | 630 | ```bash 631 | curl localhost:8080/foo -v 632 | ``` 633 | 634 | Would look like: 635 | 636 | ```bash 637 | * Trying ::1... 638 | * TCP_NODELAY set 639 | * Connected to localhost (::1) port 8080 (#0) 640 | > GET /foo HTTP/1.1 641 | > Host: localhost:8080 642 | > User-Agent: curl/7.59.0 643 | > Accept: */* 644 | > 645 | < HTTP/1.1 200 OK 646 | < Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS 647 | < Access-Control-Allow-Origin: * 648 | < Date: Fri, 28 Jun 2019 20:13:30 GMT 649 | < Content-Length: 3 650 | < Content-Type: text/plain; charset=utf-8 651 | < 652 | * Connection #0 to host localhost left intact 653 | foo 654 | ``` 655 | 656 | ### Testing Handlers 657 | 658 | Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. 659 | 660 | First, our simple HTTP handler: 661 | 662 | ```go 663 | // endpoints.go 664 | package main 665 | 666 | func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 667 | // A very simple health check. 668 | w.Header().Set("Content-Type", "application/json") 669 | w.WriteHeader(http.StatusOK) 670 | 671 | // In the future we could report back on the status of our DB, or our cache 672 | // (e.g. Redis) by performing a simple PING, and include them in the response. 673 | io.WriteString(w, `{"alive": true}`) 674 | } 675 | 676 | func main() { 677 | r := mux.NewRouter() 678 | r.HandleFunc("/health", HealthCheckHandler) 679 | 680 | log.Fatal(http.ListenAndServe("localhost:8080", r)) 681 | } 682 | ``` 683 | 684 | Our test code: 685 | 686 | ```go 687 | // endpoints_test.go 688 | package main 689 | 690 | import ( 691 | "net/http" 692 | "net/http/httptest" 693 | "testing" 694 | ) 695 | 696 | func TestHealthCheckHandler(t *testing.T) { 697 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 698 | // pass 'nil' as the third parameter. 699 | req, err := http.NewRequest("GET", "/health", nil) 700 | if err != nil { 701 | t.Fatal(err) 702 | } 703 | 704 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 705 | rr := httptest.NewRecorder() 706 | handler := http.HandlerFunc(HealthCheckHandler) 707 | 708 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 709 | // directly and pass in our Request and ResponseRecorder. 710 | handler.ServeHTTP(rr, req) 711 | 712 | // Check the status code is what we expect. 713 | if status := rr.Code; status != http.StatusOK { 714 | t.Errorf("handler returned wrong status code: got %v want %v", 715 | status, http.StatusOK) 716 | } 717 | 718 | // Check the response body is what we expect. 719 | expected := `{"alive": true}` 720 | if rr.Body.String() != expected { 721 | t.Errorf("handler returned unexpected body: got %v want %v", 722 | rr.Body.String(), expected) 723 | } 724 | } 725 | ``` 726 | 727 | In the case that our routes have [variables](#examples), we can pass those in the request. We could write 728 | [table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple 729 | possible route variables as needed. 730 | 731 | ```go 732 | // endpoints.go 733 | func main() { 734 | r := mux.NewRouter() 735 | // A route with a route variable: 736 | r.HandleFunc("/metrics/{type}", MetricsHandler) 737 | 738 | log.Fatal(http.ListenAndServe("localhost:8080", r)) 739 | } 740 | ``` 741 | 742 | Our test file, with a table-driven test of `routeVariables`: 743 | 744 | ```go 745 | // endpoints_test.go 746 | func TestMetricsHandler(t *testing.T) { 747 | tt := []struct{ 748 | routeVariable string 749 | shouldPass bool 750 | }{ 751 | {"goroutines", true}, 752 | {"heap", true}, 753 | {"counters", true}, 754 | {"queries", true}, 755 | {"adhadaeqm3k", false}, 756 | } 757 | 758 | for _, tc := range tt { 759 | path := fmt.Sprintf("/metrics/%s", tc.routeVariable) 760 | req, err := http.NewRequest("GET", path, nil) 761 | if err != nil { 762 | t.Fatal(err) 763 | } 764 | 765 | rr := httptest.NewRecorder() 766 | 767 | // To add the vars to the context, 768 | // we need to create a router through which we can pass the request. 769 | router := mux.NewRouter() 770 | router.HandleFunc("/metrics/{type}", MetricsHandler) 771 | router.ServeHTTP(rr, req) 772 | 773 | // In this case, our MetricsHandler returns a non-200 response 774 | // for a route variable it doesn't know about. 775 | if rr.Code == http.StatusOK && !tc.shouldPass { 776 | t.Errorf("handler should have failed on routeVariable %s: got %v want %v", 777 | tc.routeVariable, rr.Code, http.StatusOK) 778 | } 779 | } 780 | } 781 | ``` 782 | 783 | ## Full Example 784 | 785 | Here's a complete, runnable example of a small `mux` based server: 786 | 787 | ```go 788 | package main 789 | 790 | import ( 791 | "net/http" 792 | "log" 793 | "github.com/gorilla/mux" 794 | ) 795 | 796 | func YourHandler(w http.ResponseWriter, r *http.Request) { 797 | w.Write([]byte("Gorilla!\n")) 798 | } 799 | 800 | func main() { 801 | r := mux.NewRouter() 802 | // Routes consist of a path and a handler function. 803 | r.HandleFunc("/", YourHandler) 804 | 805 | // Bind to a port and pass our router in 806 | log.Fatal(http.ListenAndServe(":8000", r)) 807 | } 808 | ``` 809 | 810 | ## License 811 | 812 | BSD licensed. See the LICENSE file for details. 813 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | ) 12 | 13 | func BenchmarkMux(b *testing.B) { 14 | router := new(Router) 15 | handler := func(w http.ResponseWriter, r *http.Request) {} 16 | router.HandleFunc("/v1/{v1}", handler) 17 | 18 | request, _ := http.NewRequest("GET", "/v1/anything", nil) 19 | for i := 0; i < b.N; i++ { 20 | router.ServeHTTP(nil, request) 21 | } 22 | } 23 | 24 | func BenchmarkMuxSimple(b *testing.B) { 25 | router := new(Router) 26 | handler := func(w http.ResponseWriter, r *http.Request) {} 27 | router.HandleFunc("/status", handler) 28 | 29 | testCases := []struct { 30 | name string 31 | omitRouteFromContext bool 32 | }{ 33 | { 34 | name: "default", 35 | omitRouteFromContext: false, 36 | }, 37 | { 38 | name: "omit route from ctx", 39 | omitRouteFromContext: true, 40 | }, 41 | } 42 | for _, tc := range testCases { 43 | b.Run(tc.name, func(b *testing.B) { 44 | router.OmitRouteFromContext(tc.omitRouteFromContext) 45 | 46 | request, _ := http.NewRequest("GET", "/status", nil) 47 | b.ReportAllocs() 48 | b.ResetTimer() 49 | for i := 0; i < b.N; i++ { 50 | router.ServeHTTP(nil, request) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func BenchmarkMuxAlternativeInRegexp(b *testing.B) { 57 | router := new(Router) 58 | handler := func(w http.ResponseWriter, r *http.Request) {} 59 | router.HandleFunc("/v1/{v1:(?:a|b)}", handler) 60 | 61 | requestA, _ := http.NewRequest("GET", "/v1/a", nil) 62 | requestB, _ := http.NewRequest("GET", "/v1/b", nil) 63 | for i := 0; i < b.N; i++ { 64 | router.ServeHTTP(nil, requestA) 65 | router.ServeHTTP(nil, requestB) 66 | } 67 | } 68 | 69 | func BenchmarkManyPathVariables(b *testing.B) { 70 | router := new(Router) 71 | handler := func(w http.ResponseWriter, r *http.Request) {} 72 | router.HandleFunc("/v1/{v1}/{v2}/{v3}/{v4}/{v5}", handler) 73 | 74 | matchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4/5", nil) 75 | notMatchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4", nil) 76 | recorder := httptest.NewRecorder() 77 | for i := 0; i < b.N; i++ { 78 | router.ServeHTTP(nil, matchingRequest) 79 | router.ServeHTTP(recorder, notMatchingRequest) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package mux implements a request router and dispatcher. 7 | 8 | The name mux stands for "HTTP request multiplexer". Like the standard 9 | http.ServeMux, mux.Router matches incoming requests against a list of 10 | registered routes and calls a handler for the route that matches the URL 11 | or other conditions. The main features are: 12 | 13 | - Requests can be matched based on URL host, path, path prefix, schemes, 14 | header and query values, HTTP methods or using custom matchers. 15 | - URL hosts, paths and query values can have variables with an optional 16 | regular expression. 17 | - Registered URLs can be built, or "reversed", which helps maintaining 18 | references to resources. 19 | - Routes can be used as subrouters: nested routes are only tested if the 20 | parent route matches. This is useful to define groups of routes that 21 | share common conditions like a host, a path prefix or other repeated 22 | attributes. As a bonus, this optimizes request matching. 23 | - It implements the http.Handler interface so it is compatible with the 24 | standard http.ServeMux. 25 | 26 | Let's start registering a couple of URL paths and handlers: 27 | 28 | func main() { 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", HomeHandler) 31 | r.HandleFunc("/products", ProductsHandler) 32 | r.HandleFunc("/articles", ArticlesHandler) 33 | http.Handle("/", r) 34 | } 35 | 36 | Here we register three routes mapping URL paths to handlers. This is 37 | equivalent to how http.HandleFunc() works: if an incoming request URL matches 38 | one of the paths, the corresponding handler is called passing 39 | (http.ResponseWriter, *http.Request) as parameters. 40 | 41 | Paths can have variables. They are defined using the format {name} or 42 | {name:pattern}. If a regular expression pattern is not defined, the matched 43 | variable will be anything until the next slash. For example: 44 | 45 | r := mux.NewRouter() 46 | r.HandleFunc("/products/{key}", ProductHandler) 47 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 48 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 49 | 50 | Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: 51 | 52 | r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) 53 | 54 | The names are used to create a map of route variables which can be retrieved 55 | calling mux.Vars(): 56 | 57 | vars := mux.Vars(request) 58 | category := vars["category"] 59 | 60 | Note that if any capturing groups are present, mux will panic() during parsing. To prevent 61 | this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to 62 | "/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably 63 | when capturing groups were present. 64 | 65 | And this is all you need to know about the basic usage. More advanced options 66 | are explained below. 67 | 68 | Routes can also be restricted to a domain or subdomain. Just define a host 69 | pattern to be matched. They can also have variables: 70 | 71 | r := mux.NewRouter() 72 | // Only matches if domain is "www.example.com". 73 | r.Host("www.example.com") 74 | // Matches a dynamic subdomain. 75 | r.Host("{subdomain:[a-z]+}.domain.com") 76 | 77 | There are several other matchers that can be added. To match path prefixes: 78 | 79 | r.PathPrefix("/products/") 80 | 81 | ...or HTTP methods: 82 | 83 | r.Methods("GET", "POST") 84 | 85 | ...or URL schemes: 86 | 87 | r.Schemes("https") 88 | 89 | ...or header values: 90 | 91 | r.Headers("X-Requested-With", "XMLHttpRequest") 92 | 93 | ...or query values: 94 | 95 | r.Queries("key", "value") 96 | 97 | ...or to use a custom matcher function: 98 | 99 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 100 | return r.ProtoMajor == 0 101 | }) 102 | 103 | ...and finally, it is possible to combine several matchers in a single route: 104 | 105 | r.HandleFunc("/products", ProductsHandler). 106 | Host("www.example.com"). 107 | Methods("GET"). 108 | Schemes("http") 109 | 110 | Setting the same matching conditions again and again can be boring, so we have 111 | a way to group several routes that share the same requirements. 112 | We call it "subrouting". 113 | 114 | For example, let's say we have several URLs that should only match when the 115 | host is "www.example.com". Create a route for that host and get a "subrouter" 116 | from it: 117 | 118 | r := mux.NewRouter() 119 | s := r.Host("www.example.com").Subrouter() 120 | 121 | Then register routes in the subrouter: 122 | 123 | s.HandleFunc("/products/", ProductsHandler) 124 | s.HandleFunc("/products/{key}", ProductHandler) 125 | s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 126 | 127 | The three URL paths we registered above will only be tested if the domain is 128 | "www.example.com", because the subrouter is tested first. This is not 129 | only convenient, but also optimizes request matching. You can create 130 | subrouters combining any attribute matchers accepted by a route. 131 | 132 | Subrouters can be used to create domain or path "namespaces": you define 133 | subrouters in a central place and then parts of the app can register its 134 | paths relatively to a given subrouter. 135 | 136 | There's one more thing about subroutes. When a subrouter has a path prefix, 137 | the inner routes use it as base for their paths: 138 | 139 | r := mux.NewRouter() 140 | s := r.PathPrefix("/products").Subrouter() 141 | // "/products/" 142 | s.HandleFunc("/", ProductsHandler) 143 | // "/products/{key}/" 144 | s.HandleFunc("/{key}/", ProductHandler) 145 | // "/products/{key}/details" 146 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 147 | 148 | Note that the path provided to PathPrefix() represents a "wildcard": calling 149 | PathPrefix("/static/").Handler(...) means that the handler will be passed any 150 | request that matches "/static/*". This makes it easy to serve static files with mux: 151 | 152 | func main() { 153 | var dir string 154 | 155 | flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") 156 | flag.Parse() 157 | r := mux.NewRouter() 158 | 159 | // This will serve files under http://localhost:8000/static/ 160 | r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) 161 | 162 | srv := &http.Server{ 163 | Handler: r, 164 | Addr: "127.0.0.1:8000", 165 | // Good practice: enforce timeouts for servers you create! 166 | WriteTimeout: 15 * time.Second, 167 | ReadTimeout: 15 * time.Second, 168 | } 169 | 170 | log.Fatal(srv.ListenAndServe()) 171 | } 172 | 173 | Now let's see how to build registered URLs. 174 | 175 | Routes can be named. All routes that define a name can have their URLs built, 176 | or "reversed". We define a name calling Name() on a route. For example: 177 | 178 | r := mux.NewRouter() 179 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 180 | Name("article") 181 | 182 | To build a URL, get the route and call the URL() method, passing a sequence of 183 | key/value pairs for the route variables. For the previous route, we would do: 184 | 185 | url, err := r.Get("article").URL("category", "technology", "id", "42") 186 | 187 | ...and the result will be a url.URL with the following path: 188 | 189 | "/articles/technology/42" 190 | 191 | This also works for host and query value variables: 192 | 193 | r := mux.NewRouter() 194 | r.Host("{subdomain}.domain.com"). 195 | Path("/articles/{category}/{id:[0-9]+}"). 196 | Queries("filter", "{filter}"). 197 | HandlerFunc(ArticleHandler). 198 | Name("article") 199 | 200 | // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" 201 | url, err := r.Get("article").URL("subdomain", "news", 202 | "category", "technology", 203 | "id", "42", 204 | "filter", "gorilla") 205 | 206 | All variables defined in the route are required, and their values must 207 | conform to the corresponding patterns. These requirements guarantee that a 208 | generated URL will always match a registered route -- the only exception is 209 | for explicitly defined "build-only" routes which never match. 210 | 211 | Regex support also exists for matching Headers within a route. For example, we could do: 212 | 213 | r.HeadersRegexp("Content-Type", "application/(text|json)") 214 | 215 | ...and the route will match both requests with a Content-Type of `application/json` as well as 216 | `application/text` 217 | 218 | There's also a way to build only the URL host or path for a route: 219 | use the methods URLHost() or URLPath() instead. For the previous route, 220 | we would do: 221 | 222 | // "http://news.domain.com/" 223 | host, err := r.Get("article").URLHost("subdomain", "news") 224 | 225 | // "/articles/technology/42" 226 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 227 | 228 | And if you use subrouters, host and path defined separately can be built 229 | as well: 230 | 231 | r := mux.NewRouter() 232 | s := r.Host("{subdomain}.domain.com").Subrouter() 233 | s.Path("/articles/{category}/{id:[0-9]+}"). 234 | HandlerFunc(ArticleHandler). 235 | Name("article") 236 | 237 | // "http://news.domain.com/articles/technology/42" 238 | url, err := r.Get("article").URL("subdomain", "news", 239 | "category", "technology", 240 | "id", "42") 241 | 242 | Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. 243 | 244 | type MiddlewareFunc func(http.Handler) http.Handler 245 | 246 | Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). 247 | 248 | A very basic middleware which logs the URI of the request being handled could be written as: 249 | 250 | func simpleMw(next http.Handler) http.Handler { 251 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 252 | // Do stuff here 253 | log.Println(r.RequestURI) 254 | // Call the next handler, which can be another middleware in the chain, or the final handler. 255 | next.ServeHTTP(w, r) 256 | }) 257 | } 258 | 259 | Middlewares can be added to a router using `Router.Use()`: 260 | 261 | r := mux.NewRouter() 262 | r.HandleFunc("/", handler) 263 | r.Use(simpleMw) 264 | 265 | A more complex authentication middleware, which maps session token to users, could be written as: 266 | 267 | // Define our struct 268 | type authenticationMiddleware struct { 269 | tokenUsers map[string]string 270 | } 271 | 272 | // Initialize it somewhere 273 | func (amw *authenticationMiddleware) Populate() { 274 | amw.tokenUsers["00000000"] = "user0" 275 | amw.tokenUsers["aaaaaaaa"] = "userA" 276 | amw.tokenUsers["05f717e5"] = "randomUser" 277 | amw.tokenUsers["deadbeef"] = "user0" 278 | } 279 | 280 | // Middleware function, which will be called for each request 281 | func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { 282 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 283 | token := r.Header.Get("X-Session-Token") 284 | 285 | if user, found := amw.tokenUsers[token]; found { 286 | // We found the token in our map 287 | log.Printf("Authenticated user %s\n", user) 288 | next.ServeHTTP(w, r) 289 | } else { 290 | http.Error(w, "Forbidden", http.StatusForbidden) 291 | } 292 | }) 293 | } 294 | 295 | r := mux.NewRouter() 296 | r.HandleFunc("/", handler) 297 | 298 | amw := authenticationMiddleware{tokenUsers: make(map[string]string)} 299 | amw.Populate() 300 | 301 | r.Use(amw.Middleware) 302 | 303 | Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. 304 | */ 305 | package mux 306 | -------------------------------------------------------------------------------- /example_authentication_middleware_test.go: -------------------------------------------------------------------------------- 1 | package mux_test 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | // Define our struct 11 | type authenticationMiddleware struct { 12 | tokenUsers map[string]string 13 | } 14 | 15 | // Initialize it somewhere 16 | func (amw *authenticationMiddleware) Populate() { 17 | amw.tokenUsers["00000000"] = "user0" 18 | amw.tokenUsers["aaaaaaaa"] = "userA" 19 | amw.tokenUsers["05f717e5"] = "randomUser" 20 | amw.tokenUsers["deadbeef"] = "user0" 21 | } 22 | 23 | // Middleware function, which will be called for each request 24 | func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | token := r.Header.Get("X-Session-Token") 27 | 28 | if user, found := amw.tokenUsers[token]; found { 29 | // We found the token in our map 30 | log.Printf("Authenticated user %s\n", user) 31 | next.ServeHTTP(w, r) 32 | } else { 33 | http.Error(w, "Forbidden", http.StatusForbidden) 34 | } 35 | }) 36 | } 37 | 38 | func Example_authenticationMiddleware() { 39 | r := mux.NewRouter() 40 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 41 | // Do something here 42 | }) 43 | amw := authenticationMiddleware{make(map[string]string)} 44 | amw.Populate() 45 | r.Use(amw.Middleware) 46 | } 47 | -------------------------------------------------------------------------------- /example_cors_method_middleware_test.go: -------------------------------------------------------------------------------- 1 | package mux_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | func ExampleCORSMethodMiddleware() { 12 | r := mux.NewRouter() 13 | 14 | r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { 15 | // Handle the request 16 | }).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 17 | r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Set("Access-Control-Allow-Origin", "http://example.com") 19 | w.Header().Set("Access-Control-Max-Age", "86400") 20 | }).Methods(http.MethodOptions) 21 | 22 | r.Use(mux.CORSMethodMiddleware(r)) 23 | 24 | rw := httptest.NewRecorder() 25 | req, _ := http.NewRequest("OPTIONS", "/foo", nil) // needs to be OPTIONS 26 | req.Header.Set("Access-Control-Request-Method", "POST") // needs to be non-empty 27 | req.Header.Set("Access-Control-Request-Headers", "Authorization") // needs to be non-empty 28 | req.Header.Set("Origin", "http://example.com") // needs to be non-empty 29 | 30 | r.ServeHTTP(rw, req) 31 | 32 | fmt.Println(rw.Header().Get("Access-Control-Allow-Methods")) 33 | fmt.Println(rw.Header().Get("Access-Control-Allow-Origin")) 34 | // Output: 35 | // GET,PUT,PATCH,OPTIONS 36 | // http://example.com 37 | } 38 | -------------------------------------------------------------------------------- /example_route_test.go: -------------------------------------------------------------------------------- 1 | package mux_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | // This example demonstrates setting a regular expression matcher for 11 | // the header value. A plain word will match any value that contains a 12 | // matching substring as if the pattern was wrapped with `.*`. 13 | func ExampleRoute_HeadersRegexp() { 14 | r := mux.NewRouter() 15 | route := r.NewRoute().HeadersRegexp("Accept", "html") 16 | 17 | req1, _ := http.NewRequest("GET", "example.com", nil) 18 | req1.Header.Add("Accept", "text/plain") 19 | req1.Header.Add("Accept", "text/html") 20 | 21 | req2, _ := http.NewRequest("GET", "example.com", nil) 22 | req2.Header.Set("Accept", "application/xhtml+xml") 23 | 24 | matchInfo := &mux.RouteMatch{} 25 | fmt.Printf("Match: %v %q\n", route.Match(req1, matchInfo), req1.Header["Accept"]) 26 | fmt.Printf("Match: %v %q\n", route.Match(req2, matchInfo), req2.Header["Accept"]) 27 | // Output: 28 | // Match: true ["text/plain" "text/html"] 29 | // Match: true ["application/xhtml+xml"] 30 | } 31 | 32 | // This example demonstrates setting a strict regular expression matcher 33 | // for the header value. Using the start and end of string anchors, the 34 | // value must be an exact match. 35 | func ExampleRoute_HeadersRegexp_exactMatch() { 36 | r := mux.NewRouter() 37 | route := r.NewRoute().HeadersRegexp("Origin", "^https://example.co$") 38 | 39 | yes, _ := http.NewRequest("GET", "example.co", nil) 40 | yes.Header.Set("Origin", "https://example.co") 41 | 42 | no, _ := http.NewRequest("GET", "example.co.uk", nil) 43 | no.Header.Set("Origin", "https://example.co.uk") 44 | 45 | matchInfo := &mux.RouteMatch{} 46 | fmt.Printf("Match: %v %q\n", route.Match(yes, matchInfo), yes.Header["Origin"]) 47 | fmt.Printf("Match: %v %q\n", route.Match(no, matchInfo), no.Header["Origin"]) 48 | // Output: 49 | // Match: true ["https://example.co"] 50 | // Match: false ["https://example.co.uk"] 51 | } 52 | -------------------------------------------------------------------------------- /example_route_vars_test.go: -------------------------------------------------------------------------------- 1 | package mux_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/mux" 6 | ) 7 | 8 | // This example demonstrates building a dynamic URL using 9 | // required vars and values retrieve from another source 10 | func ExampleRoute_GetVarNames() { 11 | r := mux.NewRouter() 12 | 13 | route := r.Host("{domain}"). 14 | Path("/{group}/{item_id}"). 15 | Queries("some_data1", "{some_data1}"). 16 | Queries("some_data2_and_3", "{some_data2}.{some_data3}") 17 | 18 | dataSource := func(key string) string { 19 | return "my_value_for_" + key 20 | } 21 | 22 | varNames, _ := route.GetVarNames() 23 | 24 | pairs := make([]string, 0, len(varNames)*2) 25 | 26 | for _, varName := range varNames { 27 | pairs = append(pairs, varName, dataSource(varName)) 28 | } 29 | 30 | url, err := route.URL(pairs...) 31 | if err != nil { 32 | panic(err) 33 | } 34 | fmt.Println(url.String()) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gorilla/mux 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. 9 | // Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed 10 | // to it, and then calls the handler passed as parameter to the MiddlewareFunc. 11 | type MiddlewareFunc func(http.Handler) http.Handler 12 | 13 | // middleware interface is anything which implements a MiddlewareFunc named Middleware. 14 | type middleware interface { 15 | Middleware(handler http.Handler) http.Handler 16 | } 17 | 18 | // Middleware allows MiddlewareFunc to implement the middleware interface. 19 | func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { 20 | return mw(handler) 21 | } 22 | 23 | // Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. 24 | func (r *Router) Use(mwf ...MiddlewareFunc) { 25 | for _, fn := range mwf { 26 | r.middlewares = append(r.middlewares, fn) 27 | } 28 | } 29 | 30 | // useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. 31 | func (r *Router) useInterface(mw middleware) { 32 | r.middlewares = append(r.middlewares, mw) 33 | } 34 | 35 | // RouteMiddleware ------------------------------------------------------------- 36 | 37 | // Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Route. Route middleware are executed after the Router middleware but before the Route handler. 38 | func (r *Route) Use(mwf ...MiddlewareFunc) *Route { 39 | for _, fn := range mwf { 40 | r.middlewares = append(r.middlewares, fn) 41 | } 42 | 43 | return r 44 | } 45 | 46 | // useInterface appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Route. Route middleware are executed after the Router middleware but before the Route handler. 47 | func (r *Route) useInterface(mw middleware) { 48 | r.middlewares = append(r.middlewares, mw) 49 | } 50 | 51 | // CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header 52 | // on requests for routes that have an OPTIONS method matcher to all the method matchers on 53 | // the route. Routes that do not explicitly handle OPTIONS requests will not be processed 54 | // by the middleware. See examples for usage. 55 | func CORSMethodMiddleware(r *Router) MiddlewareFunc { 56 | return func(next http.Handler) http.Handler { 57 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 58 | allMethods, err := getAllMethodsForRoute(r, req) 59 | if err == nil { 60 | for _, v := range allMethods { 61 | if v == http.MethodOptions { 62 | w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) 63 | } 64 | } 65 | } 66 | 67 | next.ServeHTTP(w, req) 68 | }) 69 | } 70 | } 71 | 72 | // getAllMethodsForRoute returns all the methods from method matchers matching a given 73 | // request. 74 | func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { 75 | var allMethods []string 76 | 77 | for _, route := range r.routes { 78 | var match RouteMatch 79 | if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { 80 | methods, err := route.GetMethods() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | allMethods = append(allMethods, methods...) 86 | } 87 | } 88 | 89 | return allMethods, nil 90 | } 91 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | type testMiddleware struct { 11 | timesCalled uint 12 | } 13 | 14 | func (tm *testMiddleware) Middleware(h http.Handler) http.Handler { 15 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | tm.timesCalled++ 17 | h.ServeHTTP(w, r) 18 | }) 19 | } 20 | 21 | func dummyHandler(w http.ResponseWriter, r *http.Request) {} 22 | 23 | func TestMiddlewareAdd(t *testing.T) { 24 | router := NewRouter() 25 | router.HandleFunc("/", dummyHandler).Methods("GET") 26 | 27 | mw := &testMiddleware{} 28 | 29 | router.useInterface(mw) 30 | if len(router.middlewares) != 1 || router.middlewares[0] != mw { 31 | t.Fatal("Middleware interface was not added correctly") 32 | } 33 | 34 | router.Use(mw.Middleware) 35 | if len(router.middlewares) != 2 { 36 | t.Fatal("Middleware method was not added correctly") 37 | } 38 | 39 | banalMw := func(handler http.Handler) http.Handler { 40 | return handler 41 | } 42 | router.Use(banalMw) 43 | if len(router.middlewares) != 3 { 44 | t.Fatal("Middleware function was not added correctly") 45 | } 46 | 47 | route := router.HandleFunc("/route", dummyHandler) 48 | route.useInterface(mw) 49 | if len(route.middlewares) != 1 { 50 | t.Fatal("Route middleware function was not added correctly") 51 | } 52 | 53 | route.Use(banalMw) 54 | if len(route.middlewares) != 2 { 55 | t.Fatal("Route middleware function was not added correctly") 56 | } 57 | } 58 | 59 | func TestMiddleware(t *testing.T) { 60 | router := NewRouter() 61 | router.HandleFunc("/", dummyHandler).Methods("GET") 62 | 63 | mw := &testMiddleware{} 64 | router.useInterface(mw) 65 | 66 | rw := NewRecorder() 67 | req := newRequest("GET", "/") 68 | 69 | t.Run("regular middleware call", func(t *testing.T) { 70 | router.ServeHTTP(rw, req) 71 | if mw.timesCalled != 1 { 72 | t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) 73 | } 74 | }) 75 | 76 | t.Run("not called for 404", func(t *testing.T) { 77 | req = newRequest("GET", "/not/found") 78 | router.ServeHTTP(rw, req) 79 | if mw.timesCalled != 1 { 80 | t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) 81 | } 82 | }) 83 | 84 | t.Run("not called for method mismatch", func(t *testing.T) { 85 | req = newRequest("POST", "/") 86 | router.ServeHTTP(rw, req) 87 | if mw.timesCalled != 1 { 88 | t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) 89 | } 90 | }) 91 | 92 | t.Run("regular call using function middleware", func(t *testing.T) { 93 | router.Use(mw.Middleware) 94 | req = newRequest("GET", "/") 95 | router.ServeHTTP(rw, req) 96 | if mw.timesCalled != 3 { 97 | t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled) 98 | } 99 | }) 100 | 101 | t.Run("regular call using route middleware func", func(t *testing.T) { 102 | router.HandleFunc("/route", dummyHandler).Use(mw.Middleware) 103 | req = newRequest("GET", "/route") 104 | router.ServeHTTP(rw, req) 105 | if mw.timesCalled != 6 { 106 | t.Fatalf("Expected %d calls, but got only %d", 6, mw.timesCalled) 107 | } 108 | }) 109 | 110 | t.Run("regular call using route middleware interface", func(t *testing.T) { 111 | router.HandleFunc("/route", dummyHandler).useInterface(mw) 112 | req = newRequest("GET", "/route") 113 | router.ServeHTTP(rw, req) 114 | if mw.timesCalled != 9 { 115 | t.Fatalf("Expected %d calls, but got only %d", 9, mw.timesCalled) 116 | } 117 | }) 118 | } 119 | 120 | func TestMiddlewareSubrouter(t *testing.T) { 121 | router := NewRouter() 122 | router.HandleFunc("/", dummyHandler).Methods("GET") 123 | 124 | subrouter := router.PathPrefix("/sub").Subrouter() 125 | subrouter.HandleFunc("/x", dummyHandler).Methods("GET") 126 | 127 | mw := &testMiddleware{} 128 | subrouter.useInterface(mw) 129 | 130 | rw := NewRecorder() 131 | req := newRequest("GET", "/") 132 | 133 | t.Run("not called for route outside subrouter", func(t *testing.T) { 134 | router.ServeHTTP(rw, req) 135 | if mw.timesCalled != 0 { 136 | t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) 137 | } 138 | }) 139 | 140 | t.Run("not called for subrouter root 404", func(t *testing.T) { 141 | req = newRequest("GET", "/sub/") 142 | router.ServeHTTP(rw, req) 143 | if mw.timesCalled != 0 { 144 | t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) 145 | } 146 | }) 147 | 148 | t.Run("called once for route inside subrouter", func(t *testing.T) { 149 | req = newRequest("GET", "/sub/x") 150 | router.ServeHTTP(rw, req) 151 | if mw.timesCalled != 1 { 152 | t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) 153 | } 154 | }) 155 | 156 | t.Run("not called for 404 inside subrouter", func(t *testing.T) { 157 | req = newRequest("GET", "/sub/not/found") 158 | router.ServeHTTP(rw, req) 159 | if mw.timesCalled != 1 { 160 | t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) 161 | } 162 | }) 163 | 164 | t.Run("middleware added to router", func(t *testing.T) { 165 | router.useInterface(mw) 166 | 167 | t.Run("called once for route outside subrouter", func(t *testing.T) { 168 | req = newRequest("GET", "/") 169 | router.ServeHTTP(rw, req) 170 | if mw.timesCalled != 2 { 171 | t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled) 172 | } 173 | }) 174 | 175 | t.Run("called twice for route inside subrouter", func(t *testing.T) { 176 | req = newRequest("GET", "/sub/x") 177 | router.ServeHTTP(rw, req) 178 | if mw.timesCalled != 4 { 179 | t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled) 180 | } 181 | }) 182 | }) 183 | } 184 | 185 | func TestMiddlewareExecution(t *testing.T) { 186 | mwStr := []byte("Middleware\n") 187 | handlerStr := []byte("Logic\n") 188 | 189 | handlerFunc := func(w http.ResponseWriter, e *http.Request) { 190 | _, err := w.Write(handlerStr) 191 | if err != nil { 192 | t.Fatalf("Failed writing HTTP response: %v", err) 193 | } 194 | } 195 | 196 | router := NewRouter() 197 | router.HandleFunc("/", handlerFunc) 198 | 199 | t.Run("responds normally without middleware", func(t *testing.T) { 200 | rw := NewRecorder() 201 | req := newRequest("GET", "/") 202 | 203 | router.ServeHTTP(rw, req) 204 | 205 | if !bytes.Equal(rw.Body.Bytes(), handlerStr) { 206 | t.Fatal("Handler response is not what it should be") 207 | } 208 | }) 209 | 210 | t.Run("responds with handler and middleware response", func(t *testing.T) { 211 | rw := NewRecorder() 212 | req := newRequest("GET", "/") 213 | 214 | router.Use(func(h http.Handler) http.Handler { 215 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 216 | _, err := w.Write(mwStr) 217 | if err != nil { 218 | t.Fatalf("Failed writing HTTP response: %v", err) 219 | } 220 | h.ServeHTTP(w, r) 221 | }) 222 | }) 223 | 224 | router.ServeHTTP(rw, req) 225 | if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) { 226 | t.Fatal("Middleware + handler response is not what it should be") 227 | } 228 | }) 229 | 230 | t.Run("responds with handler, middleware and route middleware response", func(t *testing.T) { 231 | routeMwStr := []byte("Route Middleware\n") 232 | rw := NewRecorder() 233 | req := newRequest("GET", "/route") 234 | 235 | router.HandleFunc("/route", handlerFunc).Use(func(h http.Handler) http.Handler { 236 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 237 | _, err := w.Write(routeMwStr) 238 | if err != nil { 239 | t.Fatalf("Failed writing HTTP response: %v", err) 240 | } 241 | h.ServeHTTP(w, r) 242 | }) 243 | }) 244 | 245 | router.ServeHTTP(rw, req) 246 | expectedString := append(append(mwStr, routeMwStr...), handlerStr...) 247 | if !bytes.Equal(rw.Body.Bytes(), expectedString) { 248 | fmt.Println(rw.Body.String()) 249 | t.Fatal("Middleware + handler response is not what it should be") 250 | } 251 | }) 252 | } 253 | 254 | func TestMiddlewareNotFound(t *testing.T) { 255 | mwStr := []byte("Middleware\n") 256 | handlerStr := []byte("Logic\n") 257 | 258 | router := NewRouter() 259 | router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 260 | _, err := w.Write(handlerStr) 261 | if err != nil { 262 | t.Fatalf("Failed writing HTTP response: %v", err) 263 | } 264 | }) 265 | router.Use(func(h http.Handler) http.Handler { 266 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 267 | _, err := w.Write(mwStr) 268 | if err != nil { 269 | t.Fatalf("Failed writing HTTP response: %v", err) 270 | } 271 | h.ServeHTTP(w, r) 272 | }) 273 | }) 274 | 275 | // Test not found call with default handler 276 | t.Run("not called", func(t *testing.T) { 277 | rw := NewRecorder() 278 | req := newRequest("GET", "/notfound") 279 | 280 | router.ServeHTTP(rw, req) 281 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 282 | t.Fatal("Middleware was called for a 404") 283 | } 284 | }) 285 | 286 | t.Run("not called with custom not found handler", func(t *testing.T) { 287 | rw := NewRecorder() 288 | req := newRequest("GET", "/notfound") 289 | 290 | router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 291 | _, err := rw.Write([]byte("Custom 404 handler")) 292 | if err != nil { 293 | t.Fatalf("Failed writing HTTP response: %v", err) 294 | } 295 | }) 296 | router.ServeHTTP(rw, req) 297 | 298 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 299 | t.Fatal("Middleware was called for a custom 404") 300 | } 301 | }) 302 | } 303 | 304 | func TestMiddlewareMethodMismatch(t *testing.T) { 305 | mwStr := []byte("Middleware\n") 306 | handlerStr := []byte("Logic\n") 307 | 308 | router := NewRouter() 309 | router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 310 | _, err := w.Write(handlerStr) 311 | if err != nil { 312 | t.Fatalf("Failed writing HTTP response: %v", err) 313 | } 314 | }).Methods("GET") 315 | 316 | router.Use(func(h http.Handler) http.Handler { 317 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 318 | _, err := w.Write(mwStr) 319 | if err != nil { 320 | t.Fatalf("Failed writing HTTP response: %v", err) 321 | } 322 | h.ServeHTTP(w, r) 323 | }) 324 | }) 325 | 326 | t.Run("not called", func(t *testing.T) { 327 | rw := NewRecorder() 328 | req := newRequest("POST", "/") 329 | 330 | router.ServeHTTP(rw, req) 331 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 332 | t.Fatal("Middleware was called for a method mismatch") 333 | } 334 | }) 335 | 336 | t.Run("not called with custom method not allowed handler", func(t *testing.T) { 337 | rw := NewRecorder() 338 | req := newRequest("POST", "/") 339 | 340 | router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 341 | _, err := rw.Write([]byte("Method not allowed")) 342 | if err != nil { 343 | t.Fatalf("Failed writing HTTP response: %v", err) 344 | } 345 | }) 346 | router.ServeHTTP(rw, req) 347 | 348 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 349 | t.Fatal("Middleware was called for a method mismatch") 350 | } 351 | }) 352 | } 353 | 354 | func TestMiddlewareNotFoundSubrouter(t *testing.T) { 355 | mwStr := []byte("Middleware\n") 356 | handlerStr := []byte("Logic\n") 357 | 358 | router := NewRouter() 359 | router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 360 | _, err := w.Write(handlerStr) 361 | if err != nil { 362 | t.Fatalf("Failed writing HTTP response: %v", err) 363 | } 364 | }) 365 | 366 | subrouter := router.PathPrefix("/sub/").Subrouter() 367 | subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 368 | _, err := w.Write(handlerStr) 369 | if err != nil { 370 | t.Fatalf("Failed writing HTTP response: %v", err) 371 | } 372 | }) 373 | 374 | router.Use(func(h http.Handler) http.Handler { 375 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 376 | _, err := w.Write(mwStr) 377 | if err != nil { 378 | t.Fatalf("Failed writing HTTP response: %v", err) 379 | } 380 | h.ServeHTTP(w, r) 381 | }) 382 | }) 383 | 384 | t.Run("not called", func(t *testing.T) { 385 | rw := NewRecorder() 386 | req := newRequest("GET", "/sub/notfound") 387 | 388 | router.ServeHTTP(rw, req) 389 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 390 | t.Fatal("Middleware was called for a 404") 391 | } 392 | }) 393 | 394 | t.Run("not called with custom not found handler", func(t *testing.T) { 395 | rw := NewRecorder() 396 | req := newRequest("GET", "/sub/notfound") 397 | 398 | subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 399 | _, err := rw.Write([]byte("Custom 404 handler")) 400 | if err != nil { 401 | t.Fatalf("Failed writing HTTP response: %v", err) 402 | } 403 | }) 404 | router.ServeHTTP(rw, req) 405 | 406 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 407 | t.Fatal("Middleware was called for a custom 404") 408 | } 409 | }) 410 | } 411 | 412 | func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { 413 | mwStr := []byte("Middleware\n") 414 | handlerStr := []byte("Logic\n") 415 | 416 | router := NewRouter() 417 | router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 418 | _, err := w.Write(handlerStr) 419 | if err != nil { 420 | t.Fatalf("Failed writing HTTP response: %v", err) 421 | } 422 | }) 423 | 424 | subrouter := router.PathPrefix("/sub/").Subrouter() 425 | subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { 426 | _, err := w.Write(handlerStr) 427 | if err != nil { 428 | t.Fatalf("Failed writing HTTP response: %v", err) 429 | } 430 | }).Methods("GET") 431 | 432 | router.Use(func(h http.Handler) http.Handler { 433 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 434 | _, err := w.Write(mwStr) 435 | if err != nil { 436 | t.Fatalf("Failed writing HTTP response: %v", err) 437 | } 438 | h.ServeHTTP(w, r) 439 | }) 440 | }) 441 | 442 | t.Run("not called", func(t *testing.T) { 443 | rw := NewRecorder() 444 | req := newRequest("POST", "/sub/") 445 | 446 | router.ServeHTTP(rw, req) 447 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 448 | t.Fatal("Middleware was called for a method mismatch") 449 | } 450 | }) 451 | 452 | t.Run("not called with custom method not allowed handler", func(t *testing.T) { 453 | rw := NewRecorder() 454 | req := newRequest("POST", "/sub/") 455 | 456 | router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 457 | _, err := rw.Write([]byte("Method not allowed")) 458 | if err != nil { 459 | t.Fatalf("Failed writing HTTP response: %v", err) 460 | } 461 | }) 462 | router.ServeHTTP(rw, req) 463 | 464 | if bytes.Contains(rw.Body.Bytes(), mwStr) { 465 | t.Fatal("Middleware was called for a method mismatch") 466 | } 467 | }) 468 | } 469 | 470 | func TestCORSMethodMiddleware(t *testing.T) { 471 | testCases := []struct { 472 | name string 473 | registerRoutes func(r *Router) 474 | requestHeader http.Header 475 | requestMethod string 476 | requestPath string 477 | expectedAccessControlAllowMethodsHeader string 478 | expectedResponse string 479 | }{ 480 | { 481 | name: "does not set without OPTIONS matcher", 482 | registerRoutes: func(r *Router) { 483 | r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 484 | }, 485 | requestMethod: "GET", 486 | requestPath: "/foo", 487 | expectedAccessControlAllowMethodsHeader: "", 488 | expectedResponse: "a", 489 | }, 490 | { 491 | name: "sets on non OPTIONS", 492 | registerRoutes: func(r *Router) { 493 | r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 494 | r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) 495 | }, 496 | requestMethod: "GET", 497 | requestPath: "/foo", 498 | expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", 499 | expectedResponse: "a", 500 | }, 501 | { 502 | name: "sets without preflight headers", 503 | registerRoutes: func(r *Router) { 504 | r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 505 | r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) 506 | }, 507 | requestMethod: "OPTIONS", 508 | requestPath: "/foo", 509 | expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", 510 | expectedResponse: "b", 511 | }, 512 | { 513 | name: "does not set on error", 514 | registerRoutes: func(r *Router) { 515 | r.HandleFunc("/foo", stringHandler("a")) 516 | }, 517 | requestMethod: "OPTIONS", 518 | requestPath: "/foo", 519 | expectedAccessControlAllowMethodsHeader: "", 520 | expectedResponse: "a", 521 | }, 522 | { 523 | name: "sets header on valid preflight", 524 | registerRoutes: func(r *Router) { 525 | r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 526 | r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) 527 | }, 528 | requestMethod: "OPTIONS", 529 | requestPath: "/foo", 530 | requestHeader: http.Header{ 531 | "Access-Control-Request-Method": []string{"GET"}, 532 | "Access-Control-Request-Headers": []string{"Authorization"}, 533 | "Origin": []string{"http://example.com"}, 534 | }, 535 | expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", 536 | expectedResponse: "b", 537 | }, 538 | { 539 | name: "does not set methods from unmatching routes", 540 | registerRoutes: func(r *Router) { 541 | r.HandleFunc("/foo", stringHandler("c")).Methods(http.MethodDelete) 542 | r.HandleFunc("/foo/bar", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) 543 | r.HandleFunc("/foo/bar", stringHandler("b")).Methods(http.MethodOptions) 544 | }, 545 | requestMethod: "OPTIONS", 546 | requestPath: "/foo/bar", 547 | requestHeader: http.Header{ 548 | "Access-Control-Request-Method": []string{"GET"}, 549 | "Access-Control-Request-Headers": []string{"Authorization"}, 550 | "Origin": []string{"http://example.com"}, 551 | }, 552 | expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", 553 | expectedResponse: "b", 554 | }, 555 | } 556 | 557 | for _, tt := range testCases { 558 | t.Run(tt.name, func(t *testing.T) { 559 | router := NewRouter() 560 | 561 | tt.registerRoutes(router) 562 | 563 | router.Use(CORSMethodMiddleware(router)) 564 | 565 | rw := NewRecorder() 566 | req := newRequest(tt.requestMethod, tt.requestPath) 567 | req.Header = tt.requestHeader 568 | 569 | router.ServeHTTP(rw, req) 570 | 571 | actualMethodsHeader := rw.Header().Get("Access-Control-Allow-Methods") 572 | if actualMethodsHeader != tt.expectedAccessControlAllowMethodsHeader { 573 | t.Fatalf("Expected Access-Control-Allow-Methods to equal %s but got %s", tt.expectedAccessControlAllowMethodsHeader, actualMethodsHeader) 574 | } 575 | 576 | actualResponse := rw.Body.String() 577 | if actualResponse != tt.expectedResponse { 578 | t.Fatalf("Expected response to equal %s but got %s", tt.expectedResponse, actualResponse) 579 | } 580 | }) 581 | } 582 | } 583 | 584 | func TestCORSMethodMiddlewareSubrouter(t *testing.T) { 585 | router := NewRouter().StrictSlash(true) 586 | 587 | subrouter := router.PathPrefix("/test").Subrouter() 588 | subrouter.HandleFunc("/hello", stringHandler("a")).Methods(http.MethodGet, http.MethodOptions, http.MethodPost) 589 | subrouter.HandleFunc("/hello/{name}", stringHandler("b")).Methods(http.MethodGet, http.MethodOptions) 590 | 591 | subrouter.Use(CORSMethodMiddleware(subrouter)) 592 | 593 | rw := NewRecorder() 594 | req := newRequest("GET", "/test/hello/asdf") 595 | router.ServeHTTP(rw, req) 596 | 597 | actualMethods := rw.Header().Get("Access-Control-Allow-Methods") 598 | expectedMethods := "GET,OPTIONS" 599 | if actualMethods != expectedMethods { 600 | t.Fatalf("expected methods %q but got: %q", expectedMethods, actualMethods) 601 | } 602 | } 603 | 604 | func TestMiddlewareOnMultiSubrouter(t *testing.T) { 605 | first := "first" 606 | second := "second" 607 | notFound := "404 not found" 608 | 609 | router := NewRouter() 610 | firstSubRouter := router.PathPrefix("/").Subrouter() 611 | secondSubRouter := router.PathPrefix("/").Subrouter() 612 | 613 | router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 614 | _, err := rw.Write([]byte(notFound)) 615 | if err != nil { 616 | t.Fatalf("Failed writing HTTP response: %v", err) 617 | } 618 | }) 619 | 620 | firstSubRouter.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { 621 | 622 | }) 623 | 624 | secondSubRouter.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { 625 | 626 | }) 627 | 628 | firstSubRouter.Use(func(h http.Handler) http.Handler { 629 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 630 | _, err := w.Write([]byte(first)) 631 | if err != nil { 632 | t.Fatalf("Failed writing HTTP response: %v", err) 633 | } 634 | h.ServeHTTP(w, r) 635 | }) 636 | }) 637 | 638 | secondSubRouter.Use(func(h http.Handler) http.Handler { 639 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 640 | _, err := w.Write([]byte(second)) 641 | if err != nil { 642 | t.Fatalf("Failed writing HTTP response: %v", err) 643 | } 644 | h.ServeHTTP(w, r) 645 | }) 646 | }) 647 | 648 | t.Run("/first uses first middleware", func(t *testing.T) { 649 | rw := NewRecorder() 650 | req := newRequest("GET", "/first") 651 | 652 | router.ServeHTTP(rw, req) 653 | if rw.Body.String() != first { 654 | t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String()) 655 | } 656 | }) 657 | 658 | t.Run("/second uses second middleware", func(t *testing.T) { 659 | rw := NewRecorder() 660 | req := newRequest("GET", "/second") 661 | 662 | router.ServeHTTP(rw, req) 663 | if rw.Body.String() != second { 664 | t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String()) 665 | } 666 | }) 667 | 668 | t.Run("uses not found handler", func(t *testing.T) { 669 | rw := NewRecorder() 670 | req := newRequest("GET", "/second/not-exist") 671 | 672 | router.ServeHTTP(rw, req) 673 | if rw.Body.String() != notFound { 674 | t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String()) 675 | } 676 | }) 677 | } 678 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "path" 14 | "regexp" 15 | ) 16 | 17 | var ( 18 | // ErrMethodMismatch is returned when the method in the request does not match 19 | // the method defined against the route. 20 | ErrMethodMismatch = errors.New("method is not allowed") 21 | // ErrNotFound is returned when no route match is found. 22 | ErrNotFound = errors.New("no matching route was found") 23 | // RegexpCompileFunc aliases regexp.Compile and enables overriding it. 24 | // Do not run this function from `init()` in importable packages. 25 | // Changing this value is not safe for concurrent use. 26 | RegexpCompileFunc = regexp.Compile 27 | // ErrMetadataKeyNotFound is returned when the specified metadata key is not present in the map 28 | ErrMetadataKeyNotFound = errors.New("key not found in metadata") 29 | ) 30 | 31 | // NewRouter returns a new router instance. 32 | func NewRouter() *Router { 33 | return &Router{namedRoutes: make(map[string]*Route)} 34 | } 35 | 36 | // Router registers routes to be matched and dispatches a handler. 37 | // 38 | // It implements the http.Handler interface, so it can be registered to serve 39 | // requests: 40 | // 41 | // var router = mux.NewRouter() 42 | // 43 | // func main() { 44 | // http.Handle("/", router) 45 | // } 46 | // 47 | // Or, for Google App Engine, register it in a init() function: 48 | // 49 | // func init() { 50 | // http.Handle("/", router) 51 | // } 52 | // 53 | // This will send all incoming requests to the router. 54 | type Router struct { 55 | // Configurable Handler to be used when no route matches. 56 | // This can be used to render your own 404 Not Found errors. 57 | NotFoundHandler http.Handler 58 | 59 | // Configurable Handler to be used when the request method does not match the route. 60 | // This can be used to render your own 405 Method Not Allowed errors. 61 | MethodNotAllowedHandler http.Handler 62 | 63 | // Routes to be matched, in order. 64 | routes []*Route 65 | 66 | // Routes by name for URL building. 67 | namedRoutes map[string]*Route 68 | 69 | // If true, do not clear the request context after handling the request. 70 | // 71 | // Deprecated: No effect, since the context is stored on the request itself. 72 | KeepContext bool 73 | 74 | // Slice of middlewares to be called after a match is found 75 | middlewares []middleware 76 | 77 | // configuration shared with `Route` 78 | routeConf 79 | } 80 | 81 | // common route configuration shared between `Router` and `Route` 82 | type routeConf struct { 83 | // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" 84 | useEncodedPath bool 85 | 86 | // If true, when the path pattern is "/path/", accessing "/path" will 87 | // redirect to the former and vice versa. 88 | strictSlash bool 89 | 90 | // If true, when the path pattern is "/path//to", accessing "/path//to" 91 | // will not redirect 92 | skipClean bool 93 | 94 | // If true, the http.Request context will not contain the Route. 95 | omitRouteFromContext bool 96 | 97 | // if true, the the http.Request context will not contain the router 98 | omitRouterFromContext bool 99 | 100 | // Manager for the variables from host and path. 101 | regexp routeRegexpGroup 102 | 103 | // List of matchers. 104 | matchers []matcher 105 | 106 | // The scheme used when building URLs. 107 | buildScheme string 108 | 109 | buildVarsFunc BuildVarsFunc 110 | } 111 | 112 | // returns an effective deep copy of `routeConf` 113 | func copyRouteConf(r routeConf) routeConf { 114 | c := r 115 | 116 | if r.regexp.path != nil { 117 | c.regexp.path = copyRouteRegexp(r.regexp.path) 118 | } 119 | 120 | if r.regexp.host != nil { 121 | c.regexp.host = copyRouteRegexp(r.regexp.host) 122 | } 123 | 124 | c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) 125 | for _, q := range r.regexp.queries { 126 | c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) 127 | } 128 | 129 | c.matchers = make([]matcher, len(r.matchers)) 130 | copy(c.matchers, r.matchers) 131 | 132 | return c 133 | } 134 | 135 | func copyRouteRegexp(r *routeRegexp) *routeRegexp { 136 | c := *r 137 | return &c 138 | } 139 | 140 | // Match attempts to match the given request against the router's registered routes. 141 | // 142 | // If the request matches a route of this router or one of its subrouters the Route, 143 | // Handler, and Vars fields of the the match argument are filled and this function 144 | // returns true. 145 | // 146 | // If the request does not match any of this router's or its subrouters' routes 147 | // then this function returns false. If available, a reason for the match failure 148 | // will be filled in the match argument's MatchErr field. If the match failure type 149 | // (eg: not found) has a registered handler, the handler is assigned to the Handler 150 | // field of the match argument. 151 | func (r *Router) Match(req *http.Request, match *RouteMatch) bool { 152 | for _, route := range r.routes { 153 | if route.Match(req, match) { 154 | // Build middleware chain if no error was found 155 | if match.MatchErr == nil { 156 | for i := len(r.middlewares) - 1; i >= 0; i-- { 157 | match.Handler = r.middlewares[i].Middleware(match.Handler) 158 | } 159 | } 160 | return true 161 | } 162 | } 163 | 164 | if match.MatchErr == ErrMethodMismatch { 165 | if r.MethodNotAllowedHandler != nil { 166 | match.Handler = r.MethodNotAllowedHandler 167 | return true 168 | } 169 | 170 | return false 171 | } 172 | 173 | // Closest match for a router (includes sub-routers) 174 | if r.NotFoundHandler != nil { 175 | match.Handler = r.NotFoundHandler 176 | match.MatchErr = ErrNotFound 177 | return true 178 | } 179 | 180 | match.MatchErr = ErrNotFound 181 | return false 182 | } 183 | 184 | // ServeHTTP dispatches the handler registered in the matched route. 185 | // 186 | // When there is a match, the route variables can be retrieved calling 187 | // mux.Vars(request). 188 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 189 | if !r.skipClean { 190 | path := req.URL.Path 191 | if r.useEncodedPath { 192 | path = req.URL.EscapedPath() 193 | } 194 | // Clean path to canonical form and redirect. 195 | if p := cleanPath(path); p != path { 196 | w.Header().Set("Location", replaceURLPath(req.URL, p)) 197 | w.WriteHeader(http.StatusMovedPermanently) 198 | return 199 | } 200 | } 201 | var match RouteMatch 202 | var handler http.Handler 203 | if r.Match(req, &match) { 204 | handler = match.Handler 205 | if handler != nil { 206 | // Populate context for custom handlers 207 | if r.omitRouteFromContext { 208 | // Only populate the match vars (if any) into the context. 209 | req = requestWithVars(req, match.Vars) 210 | } else { 211 | req = requestWithRouteAndVars(req, match.Route, match.Vars) 212 | } 213 | 214 | if !r.omitRouterFromContext { 215 | req = requestWithRouter(req, r) 216 | } 217 | } 218 | } 219 | 220 | if handler == nil && match.MatchErr == ErrMethodMismatch { 221 | handler = methodNotAllowedHandler() 222 | } 223 | 224 | if handler == nil { 225 | handler = http.NotFoundHandler() 226 | } 227 | 228 | handler.ServeHTTP(w, req) 229 | } 230 | 231 | // Get returns a route registered with the given name. 232 | func (r *Router) Get(name string) *Route { 233 | return r.namedRoutes[name] 234 | } 235 | 236 | // GetRoute returns a route registered with the given name. This method 237 | // was renamed to Get() and remains here for backwards compatibility. 238 | func (r *Router) GetRoute(name string) *Route { 239 | return r.namedRoutes[name] 240 | } 241 | 242 | // StrictSlash defines the trailing slash behavior for new routes. The initial 243 | // value is false. 244 | // 245 | // When true, if the route path is "/path/", accessing "/path" will perform a redirect 246 | // to the former and vice versa. In other words, your application will always 247 | // see the path as specified in the route. 248 | // 249 | // When false, if the route path is "/path", accessing "/path/" will not match 250 | // this route and vice versa. 251 | // 252 | // The redirect is a HTTP 301 (Moved Permanently). Note that when this is set for 253 | // routes with a non-idempotent method (e.g. POST, PUT), the subsequent redirected 254 | // request will be made as a GET by most clients. Use middleware or client settings 255 | // to modify this behaviour as needed. 256 | // 257 | // Special case: when a route sets a path prefix using the PathPrefix() method, 258 | // strict slash is ignored for that route because the redirect behavior can't 259 | // be determined from a prefix alone. However, any subrouters created from that 260 | // route inherit the original StrictSlash setting. 261 | func (r *Router) StrictSlash(value bool) *Router { 262 | r.strictSlash = value 263 | return r 264 | } 265 | 266 | // SkipClean defines the path cleaning behaviour for new routes. The initial 267 | // value is false. Users should be careful about which routes are not cleaned 268 | // 269 | // When true, if the route path is "/path//to", it will remain with the double 270 | // slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ 271 | // 272 | // When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will 273 | // become /fetch/http/xkcd.com/534 274 | func (r *Router) SkipClean(value bool) *Router { 275 | r.skipClean = value 276 | return r 277 | } 278 | 279 | // OmitRouteFromContext defines the behavior of omitting the Route from the 280 | // 281 | // http.Request context. 282 | // 283 | // CurrentRoute will yield nil with this option. 284 | func (r *Router) OmitRouteFromContext(value bool) *Router { 285 | r.omitRouteFromContext = value 286 | return r 287 | } 288 | 289 | // OmitRouterFromContext defines the behavior of omitting the Router from the 290 | // http.Request context. 291 | // 292 | // RouterFromRequest will yield nil with this option. 293 | func (r *Router) OmitRouterFromContext(value bool) *Router { 294 | r.omitRouterFromContext = value 295 | return r 296 | } 297 | 298 | // UseEncodedPath tells the router to match the encoded original path 299 | // to the routes. 300 | // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". 301 | // 302 | // If not called, the router will match the unencoded path to the routes. 303 | // For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" 304 | func (r *Router) UseEncodedPath() *Router { 305 | r.useEncodedPath = true 306 | return r 307 | } 308 | 309 | // ---------------------------------------------------------------------------- 310 | // Route factories 311 | // ---------------------------------------------------------------------------- 312 | 313 | // NewRoute registers an empty route. 314 | func (r *Router) NewRoute() *Route { 315 | // initialize a route with a copy of the parent router's configuration 316 | route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} 317 | r.routes = append(r.routes, route) 318 | return route 319 | } 320 | 321 | // Name registers a new route with a name. 322 | // See Route.Name(). 323 | func (r *Router) Name(name string) *Route { 324 | return r.NewRoute().Name(name) 325 | } 326 | 327 | // Handle registers a new route with a matcher for the URL path. 328 | // See Route.Path() and Route.Handler(). 329 | func (r *Router) Handle(path string, handler http.Handler) *Route { 330 | return r.NewRoute().Path(path).Handler(handler) 331 | } 332 | 333 | // HandleFunc registers a new route with a matcher for the URL path. 334 | // See Route.Path() and Route.HandlerFunc(). 335 | func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, 336 | *http.Request)) *Route { 337 | return r.NewRoute().Path(path).HandlerFunc(f) 338 | } 339 | 340 | // Headers registers a new route with a matcher for request header values. 341 | // See Route.Headers(). 342 | func (r *Router) Headers(pairs ...string) *Route { 343 | return r.NewRoute().Headers(pairs...) 344 | } 345 | 346 | // Host registers a new route with a matcher for the URL host. 347 | // See Route.Host(). 348 | func (r *Router) Host(tpl string) *Route { 349 | return r.NewRoute().Host(tpl) 350 | } 351 | 352 | // MatcherFunc registers a new route with a custom matcher function. 353 | // See Route.MatcherFunc(). 354 | func (r *Router) MatcherFunc(f MatcherFunc) *Route { 355 | return r.NewRoute().MatcherFunc(f) 356 | } 357 | 358 | // Methods registers a new route with a matcher for HTTP methods. 359 | // See Route.Methods(). 360 | func (r *Router) Methods(methods ...string) *Route { 361 | return r.NewRoute().Methods(methods...) 362 | } 363 | 364 | // Path registers a new route with a matcher for the URL path. 365 | // See Route.Path(). 366 | func (r *Router) Path(tpl string) *Route { 367 | return r.NewRoute().Path(tpl) 368 | } 369 | 370 | // PathPrefix registers a new route with a matcher for the URL path prefix. 371 | // See Route.PathPrefix(). 372 | func (r *Router) PathPrefix(tpl string) *Route { 373 | return r.NewRoute().PathPrefix(tpl) 374 | } 375 | 376 | // Queries registers a new route with a matcher for URL query values. 377 | // See Route.Queries(). 378 | func (r *Router) Queries(pairs ...string) *Route { 379 | return r.NewRoute().Queries(pairs...) 380 | } 381 | 382 | // Schemes registers a new route with a matcher for URL schemes. 383 | // See Route.Schemes(). 384 | func (r *Router) Schemes(schemes ...string) *Route { 385 | return r.NewRoute().Schemes(schemes...) 386 | } 387 | 388 | // BuildVarsFunc registers a new route with a custom function for modifying 389 | // route variables before building a URL. 390 | func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { 391 | return r.NewRoute().BuildVarsFunc(f) 392 | } 393 | 394 | // Walk walks the router and all its sub-routers, calling walkFn for each route 395 | // in the tree. The routes are walked in the order they were added. Sub-routers 396 | // are explored depth-first. 397 | func (r *Router) Walk(walkFn WalkFunc) error { 398 | return r.walk(walkFn, []*Route{}) 399 | } 400 | 401 | // SkipRouter is used as a return value from WalkFuncs to indicate that the 402 | // router that walk is about to descend down to should be skipped. 403 | var SkipRouter = errors.New("skip this router") 404 | 405 | // WalkFunc is the type of the function called for each route visited by Walk. 406 | // At every invocation, it is given the current route, and the current router, 407 | // and a list of ancestor routes that lead to the current route. 408 | type WalkFunc func(route *Route, router *Router, ancestors []*Route) error 409 | 410 | func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { 411 | for _, t := range r.routes { 412 | err := walkFn(t, r, ancestors) 413 | if err == SkipRouter { 414 | continue 415 | } 416 | if err != nil { 417 | return err 418 | } 419 | for _, sr := range t.matchers { 420 | if h, ok := sr.(*Router); ok { 421 | ancestors = append(ancestors, t) 422 | err := h.walk(walkFn, ancestors) 423 | if err != nil { 424 | return err 425 | } 426 | ancestors = ancestors[:len(ancestors)-1] 427 | } 428 | } 429 | if h, ok := t.handler.(*Router); ok { 430 | ancestors = append(ancestors, t) 431 | err := h.walk(walkFn, ancestors) 432 | if err != nil { 433 | return err 434 | } 435 | ancestors = ancestors[:len(ancestors)-1] 436 | } 437 | } 438 | return nil 439 | } 440 | 441 | // ---------------------------------------------------------------------------- 442 | // Context 443 | // ---------------------------------------------------------------------------- 444 | 445 | // RouteMatch stores information about a matched route. 446 | type RouteMatch struct { 447 | Route *Route 448 | Handler http.Handler 449 | Vars map[string]string 450 | 451 | // MatchErr is set to appropriate matching error 452 | // It is set to ErrMethodMismatch if there is a mismatch in 453 | // the request method and route method 454 | MatchErr error 455 | } 456 | 457 | type contextKey int 458 | 459 | const ( 460 | varsKey contextKey = iota 461 | routeKey 462 | routerKey 463 | ) 464 | 465 | // Vars returns the route variables for the current request, if any. 466 | func Vars(r *http.Request) map[string]string { 467 | if rv := r.Context().Value(varsKey); rv != nil { 468 | return rv.(map[string]string) 469 | } 470 | return nil 471 | } 472 | 473 | // CurrentRoute returns the matched route for the current request, if any. 474 | // This only works when called inside the handler of the matched route 475 | // because the matched route is stored in the request context which is cleared 476 | // after the handler returns. 477 | func CurrentRoute(r *http.Request) *Route { 478 | if rv := r.Context().Value(routeKey); rv != nil { 479 | return rv.(*Route) 480 | } 481 | return nil 482 | } 483 | 484 | func CurrentRouter(r *http.Request) *Router { 485 | if rv := r.Context().Value(routerKey); rv != nil { 486 | return rv.(*Router) 487 | } 488 | return nil 489 | } 490 | 491 | // requestWithVars adds the matched vars to the request ctx. 492 | // It shortcuts the operation when the vars are empty. 493 | func requestWithVars(r *http.Request, vars map[string]string) *http.Request { 494 | if len(vars) == 0 { 495 | return r 496 | } 497 | ctx := context.WithValue(r.Context(), varsKey, vars) 498 | return r.WithContext(ctx) 499 | } 500 | 501 | // requestWithRouteAndVars adds the matched route and vars to the request ctx. 502 | // It saves extra allocations in cloning the request once and skipping the 503 | // 504 | // population of empty vars, which in turn mux.Vars can handle gracefully. 505 | func requestWithRouteAndVars(r *http.Request, route *Route, vars map[string]string) *http.Request { 506 | ctx := context.WithValue(r.Context(), routeKey, route) 507 | if len(vars) > 0 { 508 | ctx = context.WithValue(ctx, varsKey, vars) 509 | } 510 | return r.WithContext(ctx) 511 | } 512 | 513 | func requestWithRouter(r *http.Request, router *Router) *http.Request { 514 | ctx := context.WithValue(r.Context(), routerKey, router) 515 | return r.WithContext(ctx) 516 | } 517 | 518 | // ---------------------------------------------------------------------------- 519 | // Helpers 520 | // ---------------------------------------------------------------------------- 521 | 522 | // cleanPath returns the canonical path for p, eliminating . and .. elements. 523 | // Borrowed from the net/http package. 524 | func cleanPath(p string) string { 525 | if p == "" { 526 | return "/" 527 | } 528 | if p[0] != '/' { 529 | p = "/" + p 530 | } 531 | np := path.Clean(p) 532 | // path.Clean removes trailing slash except for root; 533 | // put the trailing slash back if necessary. 534 | if p[len(p)-1] == '/' && np != "/" { 535 | np += "/" 536 | } 537 | 538 | return np 539 | } 540 | 541 | // replaceURLPath prints an url.URL with a different path. 542 | func replaceURLPath(u *url.URL, p string) string { 543 | // Operate on a copy of the request url. 544 | u2 := *u 545 | u2.Path = p 546 | return u2.String() 547 | } 548 | 549 | // uniqueVars returns an error if two slices contain duplicated strings. 550 | func uniqueVars(s1, s2 []string) error { 551 | for _, v1 := range s1 { 552 | for _, v2 := range s2 { 553 | if v1 == v2 { 554 | return fmt.Errorf("mux: duplicated route variable %q", v2) 555 | } 556 | } 557 | } 558 | return nil 559 | } 560 | 561 | // checkPairs returns the count of strings passed in, and an error if 562 | // the count is not an even number. 563 | func checkPairs(pairs ...string) (int, error) { 564 | length := len(pairs) 565 | if length%2 != 0 { 566 | return length, fmt.Errorf( 567 | "mux: number of parameters must be multiple of 2, got %v", pairs) 568 | } 569 | return length, nil 570 | } 571 | 572 | // mapFromPairsToString converts variadic string parameters to a 573 | // string to string map. 574 | func mapFromPairsToString(pairs ...string) (map[string]string, error) { 575 | length, err := checkPairs(pairs...) 576 | if err != nil { 577 | return nil, err 578 | } 579 | m := make(map[string]string, length/2) 580 | for i := 0; i < length; i += 2 { 581 | m[pairs[i]] = pairs[i+1] 582 | } 583 | return m, nil 584 | } 585 | 586 | // mapFromPairsToRegex converts variadic string parameters to a 587 | // string to regex map. 588 | func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { 589 | length, err := checkPairs(pairs...) 590 | if err != nil { 591 | return nil, err 592 | } 593 | m := make(map[string]*regexp.Regexp, length/2) 594 | for i := 0; i < length; i += 2 { 595 | regex, err := RegexpCompileFunc(pairs[i+1]) 596 | if err != nil { 597 | return nil, err 598 | } 599 | m[pairs[i]] = regex 600 | } 601 | return m, nil 602 | } 603 | 604 | // matchInArray returns true if the given string value is in the array. 605 | func matchInArray(arr []string, value string) bool { 606 | for _, v := range arr { 607 | if v == value { 608 | return true 609 | } 610 | } 611 | return false 612 | } 613 | 614 | // matchMapWithString returns true if the given key/value pairs exist in a given map. 615 | func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { 616 | for k, v := range toCheck { 617 | // Check if key exists. 618 | if canonicalKey { 619 | k = http.CanonicalHeaderKey(k) 620 | } 621 | if values := toMatch[k]; values == nil { 622 | return false 623 | } else if v != "" { 624 | // If value was defined as an empty string we only check that the 625 | // key exists. Otherwise we also check for equality. 626 | valueExists := false 627 | for _, value := range values { 628 | if v == value { 629 | valueExists = true 630 | break 631 | } 632 | } 633 | if !valueExists { 634 | return false 635 | } 636 | } 637 | } 638 | return true 639 | } 640 | 641 | // matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against 642 | // the given regex 643 | func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { 644 | for k, v := range toCheck { 645 | // Check if key exists. 646 | if canonicalKey { 647 | k = http.CanonicalHeaderKey(k) 648 | } 649 | if values := toMatch[k]; values == nil { 650 | return false 651 | } else if v != nil { 652 | // If value was defined as an empty string we only check that the 653 | // key exists. Otherwise we also check for equality. 654 | valueExists := false 655 | for _, value := range values { 656 | if v.MatchString(value) { 657 | valueExists = true 658 | break 659 | } 660 | } 661 | if !valueExists { 662 | return false 663 | } 664 | } 665 | } 666 | return true 667 | } 668 | 669 | // methodNotAllowed replies to the request with an HTTP status code 405. 670 | func methodNotAllowed(w http.ResponseWriter, r *http.Request) { 671 | w.WriteHeader(http.StatusMethodNotAllowed) 672 | } 673 | 674 | // methodNotAllowedHandler returns a simple request handler 675 | // that replies to each request with a status code 405. 676 | func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } 677 | -------------------------------------------------------------------------------- /mux_httpserver_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.9 2 | // +build go1.9 3 | 4 | package mux 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func TestSchemeMatchers(t *testing.T) { 15 | router := NewRouter() 16 | router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { 17 | _, err := rw.Write([]byte("hello http world")) 18 | if err != nil { 19 | t.Fatalf("Failed writing HTTP response: %v", err) 20 | } 21 | }).Schemes("http") 22 | router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { 23 | _, err := rw.Write([]byte("hello https world")) 24 | if err != nil { 25 | t.Fatalf("Failed writing HTTP response: %v", err) 26 | } 27 | }).Schemes("https") 28 | 29 | assertResponseBody := func(t *testing.T, s *httptest.Server, expectedBody string) { 30 | resp, err := s.Client().Get(s.URL) 31 | if err != nil { 32 | t.Fatalf("unexpected error getting from server: %v", err) 33 | } 34 | if resp.StatusCode != 200 { 35 | t.Fatalf("expected a status code of 200, got %v", resp.StatusCode) 36 | } 37 | body, err := io.ReadAll(resp.Body) 38 | if err != nil { 39 | t.Fatalf("unexpected error reading body: %v", err) 40 | } 41 | if !bytes.Equal(body, []byte(expectedBody)) { 42 | t.Fatalf("response should be hello world, was: %q", string(body)) 43 | } 44 | } 45 | 46 | t.Run("httpServer", func(t *testing.T) { 47 | s := httptest.NewServer(router) 48 | defer s.Close() 49 | assertResponseBody(t, s, "hello http world") 50 | }) 51 | t.Run("httpsServer", func(t *testing.T) { 52 | s := httptest.NewTLSServer(router) 53 | defer s.Close() 54 | assertResponseBody(t, s, "hello https world") 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /old_test.go: -------------------------------------------------------------------------------- 1 | // Old tests ported to Go1. This is a mess. Want to drop it one day. 2 | 3 | // Copyright 2011 Gorilla Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package mux 8 | 9 | import ( 10 | "bytes" 11 | "net/http" 12 | "testing" 13 | ) 14 | 15 | // ---------------------------------------------------------------------------- 16 | // ResponseRecorder 17 | // ---------------------------------------------------------------------------- 18 | // Copyright 2009 The Go Authors. All rights reserved. 19 | // Use of this source code is governed by a BSD-style 20 | // license that can be found in the LICENSE file. 21 | 22 | // ResponseRecorder is an implementation of http.ResponseWriter that 23 | // records its mutations for later inspection in tests. 24 | type ResponseRecorder struct { 25 | Code int // the HTTP response code from WriteHeader 26 | HeaderMap http.Header // the HTTP response headers 27 | Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to 28 | Flushed bool 29 | } 30 | 31 | // NewRecorder returns an initialized ResponseRecorder. 32 | func NewRecorder() *ResponseRecorder { 33 | return &ResponseRecorder{ 34 | HeaderMap: make(http.Header), 35 | Body: new(bytes.Buffer), 36 | } 37 | } 38 | 39 | // Header returns the response headers. 40 | func (rw *ResponseRecorder) Header() http.Header { 41 | return rw.HeaderMap 42 | } 43 | 44 | // Write always succeeds and writes to rw.Body, if not nil. 45 | func (rw *ResponseRecorder) Write(buf []byte) (int, error) { 46 | if rw.Body != nil { 47 | rw.Body.Write(buf) 48 | } 49 | if rw.Code == 0 { 50 | rw.Code = http.StatusOK 51 | } 52 | return len(buf), nil 53 | } 54 | 55 | // WriteHeader sets rw.Code. 56 | func (rw *ResponseRecorder) WriteHeader(code int) { 57 | rw.Code = code 58 | } 59 | 60 | // Flush sets rw.Flushed to true. 61 | func (rw *ResponseRecorder) Flush() { 62 | rw.Flushed = true 63 | } 64 | 65 | // ---------------------------------------------------------------------------- 66 | 67 | func TestRouteMatchers(t *testing.T) { 68 | var scheme, host, path, query, method string 69 | var headers map[string]string 70 | var resultVars map[bool]map[string]string 71 | 72 | router := NewRouter() 73 | router.NewRoute().Host("{var1}.google.com"). 74 | Path("/{var2:[a-z]+}/{var3:[0-9]+}"). 75 | Queries("foo", "bar"). 76 | Methods("GET"). 77 | Schemes("https"). 78 | Headers("x-requested-with", "XMLHttpRequest") 79 | router.NewRoute().Host("www.{var4}.com"). 80 | PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). 81 | Queries("baz", "ding"). 82 | Methods("POST"). 83 | Schemes("http"). 84 | Headers("Content-Type", "application/json") 85 | 86 | reset := func() { 87 | // Everything match. 88 | scheme = "https" 89 | host = "www.google.com" 90 | path = "/product/42" 91 | query = "?foo=bar" 92 | method = "GET" 93 | headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} 94 | resultVars = map[bool]map[string]string{ 95 | true: {"var1": "www", "var2": "product", "var3": "42"}, 96 | false: {}, 97 | } 98 | } 99 | 100 | reset2 := func() { 101 | // Everything match. 102 | scheme = "http" 103 | host = "www.google.com" 104 | path = "/foo/product/42/path/that/is/ignored" 105 | query = "?baz=ding" 106 | method = "POST" 107 | headers = map[string]string{"Content-Type": "application/json"} 108 | resultVars = map[bool]map[string]string{ 109 | true: {"var4": "google", "var5": "product", "var6": "42"}, 110 | false: {}, 111 | } 112 | } 113 | 114 | match := func(shouldMatch bool) { 115 | url := scheme + "://" + host + path + query 116 | request, _ := http.NewRequest(method, url, nil) 117 | for key, value := range headers { 118 | request.Header.Add(key, value) 119 | } 120 | 121 | var routeMatch RouteMatch 122 | matched := router.Match(request, &routeMatch) 123 | if matched != shouldMatch { 124 | t.Errorf("Expected: %v\nGot: %v\nRequest: %v %v", shouldMatch, matched, request.Method, url) 125 | } 126 | 127 | if matched { 128 | currentRoute := routeMatch.Route 129 | if currentRoute == nil { 130 | t.Errorf("Expected a current route.") 131 | } 132 | vars := routeMatch.Vars 133 | expectedVars := resultVars[shouldMatch] 134 | if len(vars) != len(expectedVars) { 135 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 136 | } 137 | for name, value := range vars { 138 | if expectedVars[name] != value { 139 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 140 | } 141 | } 142 | } 143 | } 144 | 145 | // 1st route -------------------------------------------------------------- 146 | 147 | // Everything match. 148 | reset() 149 | match(true) 150 | 151 | // Scheme doesn't match. 152 | reset() 153 | scheme = "http" 154 | match(false) 155 | 156 | // Host doesn't match. 157 | reset() 158 | host = "www.mygoogle.com" 159 | match(false) 160 | 161 | // Path doesn't match. 162 | reset() 163 | path = "/product/notdigits" 164 | match(false) 165 | 166 | // Query doesn't match. 167 | reset() 168 | query = "?foo=baz" 169 | match(false) 170 | 171 | // Method doesn't match. 172 | reset() 173 | method = "POST" 174 | match(false) 175 | 176 | // Header doesn't match. 177 | reset() 178 | headers = map[string]string{} 179 | match(false) 180 | 181 | // Everything match, again. 182 | reset() 183 | match(true) 184 | 185 | // 2nd route -------------------------------------------------------------- 186 | // Everything match. 187 | reset2() 188 | match(true) 189 | 190 | // Scheme doesn't match. 191 | reset2() 192 | scheme = "https" 193 | match(false) 194 | 195 | // Host doesn't match. 196 | reset2() 197 | host = "sub.google.com" 198 | match(false) 199 | 200 | // Path doesn't match. 201 | reset2() 202 | path = "/bar/product/42" 203 | match(false) 204 | 205 | // Query doesn't match. 206 | reset2() 207 | query = "?foo=baz" 208 | match(false) 209 | 210 | // Method doesn't match. 211 | reset2() 212 | method = "GET" 213 | match(false) 214 | 215 | // Header doesn't match. 216 | reset2() 217 | headers = map[string]string{} 218 | match(false) 219 | 220 | // Everything match, again. 221 | reset2() 222 | match(true) 223 | } 224 | 225 | type headerMatcherTest struct { 226 | matcher headerMatcher 227 | headers map[string]string 228 | result bool 229 | } 230 | 231 | var headerMatcherTests = []headerMatcherTest{ 232 | { 233 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 234 | headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, 235 | result: true, 236 | }, 237 | { 238 | matcher: headerMatcher(map[string]string{"x-requested-with": ""}), 239 | headers: map[string]string{"X-Requested-With": "anything"}, 240 | result: true, 241 | }, 242 | { 243 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 244 | headers: map[string]string{}, 245 | result: false, 246 | }, 247 | } 248 | 249 | type hostMatcherTest struct { 250 | matcher *Route 251 | url string 252 | vars map[string]string 253 | result bool 254 | } 255 | 256 | var hostMatcherTests = []hostMatcherTest{ 257 | { 258 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), 259 | url: "http://abc.def.ghi/", 260 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 261 | result: true, 262 | }, 263 | { 264 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}:{port:.*}"), 265 | url: "http://abc.def.ghi:65535/", 266 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi", "port": "65535"}, 267 | result: true, 268 | }, 269 | { 270 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), 271 | url: "http://abc.def.ghi:65535/", 272 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 273 | result: true, 274 | }, 275 | { 276 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), 277 | url: "http://a.b.c/", 278 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 279 | result: false, 280 | }, 281 | } 282 | 283 | type methodMatcherTest struct { 284 | matcher methodMatcher 285 | method string 286 | result bool 287 | } 288 | 289 | var methodMatcherTests = []methodMatcherTest{ 290 | { 291 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 292 | method: "GET", 293 | result: true, 294 | }, 295 | { 296 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 297 | method: "POST", 298 | result: true, 299 | }, 300 | { 301 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 302 | method: "PUT", 303 | result: true, 304 | }, 305 | { 306 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 307 | method: "DELETE", 308 | result: false, 309 | }, 310 | } 311 | 312 | type pathMatcherTest struct { 313 | matcher *Route 314 | url string 315 | vars map[string]string 316 | result bool 317 | } 318 | 319 | var pathMatcherTests = []pathMatcherTest{ 320 | { 321 | matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), 322 | url: "http://localhost:8080/123/456/789", 323 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 324 | result: true, 325 | }, 326 | { 327 | matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), 328 | url: "http://localhost:8080/1/2/3", 329 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 330 | result: false, 331 | }, 332 | } 333 | 334 | type schemeMatcherTest struct { 335 | matcher schemeMatcher 336 | url string 337 | result bool 338 | } 339 | 340 | var schemeMatcherTests = []schemeMatcherTest{ 341 | { 342 | matcher: schemeMatcher([]string{"http", "https"}), 343 | url: "http://localhost:8080/", 344 | result: true, 345 | }, 346 | { 347 | matcher: schemeMatcher([]string{"http", "https"}), 348 | url: "https://localhost:8080/", 349 | result: true, 350 | }, 351 | { 352 | matcher: schemeMatcher([]string{"https"}), 353 | url: "http://localhost:8080/", 354 | result: false, 355 | }, 356 | { 357 | matcher: schemeMatcher([]string{"http"}), 358 | url: "https://localhost:8080/", 359 | result: false, 360 | }, 361 | } 362 | 363 | type urlBuildingTest struct { 364 | route *Route 365 | vars []string 366 | url string 367 | } 368 | 369 | var urlBuildingTests = []urlBuildingTest{ 370 | { 371 | route: new(Route).Host("foo.domain.com"), 372 | vars: []string{}, 373 | url: "http://foo.domain.com", 374 | }, 375 | { 376 | route: new(Route).Host("{subdomain}.domain.com"), 377 | vars: []string{"subdomain", "bar"}, 378 | url: "http://bar.domain.com", 379 | }, 380 | { 381 | route: new(Route).Host("{subdomain}.domain.com:{port:.*}"), 382 | vars: []string{"subdomain", "bar", "port", "65535"}, 383 | url: "http://bar.domain.com:65535", 384 | }, 385 | { 386 | route: new(Route).Host("foo.domain.com").Path("/articles"), 387 | vars: []string{}, 388 | url: "http://foo.domain.com/articles", 389 | }, 390 | { 391 | route: new(Route).Path("/articles"), 392 | vars: []string{}, 393 | url: "/articles", 394 | }, 395 | { 396 | route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), 397 | vars: []string{"category", "technology", "id", "42"}, 398 | url: "/articles/technology/42", 399 | }, 400 | { 401 | route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), 402 | vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, 403 | url: "http://foo.domain.com/articles/technology/42", 404 | }, 405 | { 406 | route: new(Route).Host("example.com").Schemes("https", "http"), 407 | vars: []string{}, 408 | url: "https://example.com", 409 | }, 410 | } 411 | 412 | func TestHeaderMatcher(t *testing.T) { 413 | for _, v := range headerMatcherTests { 414 | request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) 415 | for key, value := range v.headers { 416 | request.Header.Add(key, value) 417 | } 418 | var routeMatch RouteMatch 419 | result := v.matcher.Match(request, &routeMatch) 420 | if result != v.result { 421 | if v.result { 422 | t.Errorf("%#v: should match %v.", v.matcher, request.Header) 423 | } else { 424 | t.Errorf("%#v: should not match %v.", v.matcher, request.Header) 425 | } 426 | } 427 | } 428 | } 429 | 430 | func TestHostMatcher(t *testing.T) { 431 | for _, v := range hostMatcherTests { 432 | request, err := http.NewRequest("GET", v.url, nil) 433 | if err != nil { 434 | t.Errorf("http.NewRequest failed %#v", err) 435 | continue 436 | } 437 | var routeMatch RouteMatch 438 | result := v.matcher.Match(request, &routeMatch) 439 | vars := routeMatch.Vars 440 | if result != v.result { 441 | if v.result { 442 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 443 | } else { 444 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 445 | } 446 | } 447 | if result { 448 | if len(vars) != len(v.vars) { 449 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 450 | } 451 | for name, value := range vars { 452 | if v.vars[name] != value { 453 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 454 | } 455 | } 456 | } else { 457 | if len(vars) != 0 { 458 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 459 | } 460 | } 461 | } 462 | } 463 | 464 | func TestMethodMatcher(t *testing.T) { 465 | for _, v := range methodMatcherTests { 466 | request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil) 467 | var routeMatch RouteMatch 468 | result := v.matcher.Match(request, &routeMatch) 469 | if result != v.result { 470 | if v.result { 471 | t.Errorf("%#v: should match %v.", v.matcher, v.method) 472 | } else { 473 | t.Errorf("%#v: should not match %v.", v.matcher, v.method) 474 | } 475 | } 476 | } 477 | } 478 | 479 | func TestPathMatcher(t *testing.T) { 480 | for _, v := range pathMatcherTests { 481 | request, _ := http.NewRequest("GET", v.url, nil) 482 | var routeMatch RouteMatch 483 | result := v.matcher.Match(request, &routeMatch) 484 | vars := routeMatch.Vars 485 | if result != v.result { 486 | if v.result { 487 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 488 | } else { 489 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 490 | } 491 | } 492 | if result { 493 | if len(vars) != len(v.vars) { 494 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 495 | } 496 | for name, value := range vars { 497 | if v.vars[name] != value { 498 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 499 | } 500 | } 501 | } else { 502 | if len(vars) != 0 { 503 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 504 | } 505 | } 506 | } 507 | } 508 | 509 | func TestSchemeMatcher(t *testing.T) { 510 | for _, v := range schemeMatcherTests { 511 | request, _ := http.NewRequest("GET", v.url, nil) 512 | var routeMatch RouteMatch 513 | result := v.matcher.Match(request, &routeMatch) 514 | if result != v.result { 515 | if v.result { 516 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 517 | } else { 518 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 519 | } 520 | } 521 | } 522 | } 523 | 524 | func TestUrlBuilding(t *testing.T) { 525 | 526 | for _, v := range urlBuildingTests { 527 | u, _ := v.route.URL(v.vars...) 528 | url := u.String() 529 | if url != v.url { 530 | t.Errorf("expected %v, got %v", v.url, url) 531 | } 532 | } 533 | 534 | ArticleHandler := func(w http.ResponseWriter, r *http.Request) { 535 | } 536 | 537 | router := NewRouter() 538 | router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") 539 | 540 | url, _ := router.Get("article").URL("category", "technology", "id", "42") 541 | expected := "/articles/technology/42" 542 | if url.String() != expected { 543 | t.Errorf("Expected %v, got %v", expected, url.String()) 544 | } 545 | } 546 | 547 | func TestMatchedRouteName(t *testing.T) { 548 | routeName := "stock" 549 | router := NewRouter() 550 | route := router.NewRoute().Path("/products/").Name(routeName) 551 | 552 | url := "http://www.example.com/products/" 553 | request, _ := http.NewRequest("GET", url, nil) 554 | var rv RouteMatch 555 | ok := router.Match(request, &rv) 556 | 557 | if !ok || rv.Route != route { 558 | t.Errorf("Expected same route, got %+v.", rv.Route) 559 | } 560 | 561 | retName := rv.Route.GetName() 562 | if retName != routeName { 563 | t.Errorf("Expected %q, got %q.", routeName, retName) 564 | } 565 | } 566 | 567 | func TestSubRouting(t *testing.T) { 568 | // Example from docs. 569 | router := NewRouter() 570 | subrouter := router.NewRoute().Host("www.example.com").Subrouter() 571 | route := subrouter.NewRoute().Path("/products/").Name("products") 572 | 573 | url := "http://www.example.com/products/" 574 | request, _ := http.NewRequest("GET", url, nil) 575 | var rv RouteMatch 576 | ok := router.Match(request, &rv) 577 | 578 | if !ok || rv.Route != route { 579 | t.Errorf("Expected same route, got %+v.", rv.Route) 580 | } 581 | 582 | u, _ := router.Get("products").URL() 583 | builtURL := u.String() 584 | // Yay, subroute aware of the domain when building! 585 | if builtURL != url { 586 | t.Errorf("Expected %q, got %q.", url, builtURL) 587 | } 588 | } 589 | 590 | func TestVariableNames(t *testing.T) { 591 | route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") 592 | if route.err == nil { 593 | t.Errorf("Expected error for duplicated variable names") 594 | } 595 | } 596 | 597 | func TestRedirectSlash(t *testing.T) { 598 | var route *Route 599 | var routeMatch RouteMatch 600 | r := NewRouter() 601 | 602 | r.StrictSlash(false) 603 | route = r.NewRoute() 604 | if route.strictSlash != false { 605 | t.Errorf("Expected false redirectSlash.") 606 | } 607 | 608 | r.StrictSlash(true) 609 | route = r.NewRoute() 610 | if route.strictSlash != true { 611 | t.Errorf("Expected true redirectSlash.") 612 | } 613 | 614 | route = new(Route) 615 | route.strictSlash = true 616 | route.Path("/{arg1}/{arg2:[0-9]+}/") 617 | request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) 618 | routeMatch = RouteMatch{} 619 | _ = route.Match(request, &routeMatch) 620 | vars := routeMatch.Vars 621 | if vars["arg1"] != "foo" { 622 | t.Errorf("Expected foo.") 623 | } 624 | if vars["arg2"] != "123" { 625 | t.Errorf("Expected 123.") 626 | } 627 | rsp := NewRecorder() 628 | routeMatch.Handler.ServeHTTP(rsp, request) 629 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { 630 | t.Errorf("Expected redirect header.") 631 | } 632 | 633 | route = new(Route) 634 | route.strictSlash = true 635 | route.Path("/{arg1}/{arg2:[0-9]+}") 636 | request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) 637 | routeMatch = RouteMatch{} 638 | _ = route.Match(request, &routeMatch) 639 | vars = routeMatch.Vars 640 | if vars["arg1"] != "foo" { 641 | t.Errorf("Expected foo.") 642 | } 643 | if vars["arg2"] != "123" { 644 | t.Errorf("Expected 123.") 645 | } 646 | rsp = NewRecorder() 647 | routeMatch.Handler.ServeHTTP(rsp, request) 648 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { 649 | t.Errorf("Expected redirect header.") 650 | } 651 | } 652 | 653 | // Test for the new regexp library, still not available in stable Go. 654 | func TestNewRegexp(t *testing.T) { 655 | var p *routeRegexp 656 | var matches []string 657 | 658 | tests := map[string]map[string][]string{ 659 | "/{foo:a{2}}": { 660 | "/a": nil, 661 | "/aa": {"aa"}, 662 | "/aaa": nil, 663 | "/aaaa": nil, 664 | }, 665 | "/{foo:a{2,}}": { 666 | "/a": nil, 667 | "/aa": {"aa"}, 668 | "/aaa": {"aaa"}, 669 | "/aaaa": {"aaaa"}, 670 | }, 671 | "/{foo:a{2,3}}": { 672 | "/a": nil, 673 | "/aa": {"aa"}, 674 | "/aaa": {"aaa"}, 675 | "/aaaa": nil, 676 | }, 677 | "/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { 678 | "/a": nil, 679 | "/ab": nil, 680 | "/abc": nil, 681 | "/abcd": nil, 682 | "/abc/ab": {"abc", "ab"}, 683 | "/abc/abc": nil, 684 | "/abcd/ab": nil, 685 | }, 686 | `/{foo:\w{3,}}/{bar:\d{2,}}`: { 687 | "/a": nil, 688 | "/ab": nil, 689 | "/abc": nil, 690 | "/abc/1": nil, 691 | "/abc/12": {"abc", "12"}, 692 | "/abcd/12": {"abcd", "12"}, 693 | "/abcd/123": {"abcd", "123"}, 694 | }, 695 | } 696 | 697 | for pattern, paths := range tests { 698 | p, _ = newRouteRegexp(pattern, regexpTypePath, routeRegexpOptions{}) 699 | for path, result := range paths { 700 | matches = p.regexp.FindStringSubmatch(path) 701 | if result == nil { 702 | if matches != nil { 703 | t.Errorf("%v should not match %v.", pattern, path) 704 | } 705 | } else { 706 | if len(matches) != len(result)+1 { 707 | t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) 708 | } else { 709 | for k, v := range result { 710 | if matches[k+1] != v { 711 | t.Errorf("Expected %v, got %v.", v, matches[k+1]) 712 | } 713 | } 714 | } 715 | } 716 | } 717 | } 718 | } 719 | -------------------------------------------------------------------------------- /regexp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | type routeRegexpOptions struct { 18 | strictSlash bool 19 | useEncodedPath bool 20 | } 21 | 22 | type regexpType int 23 | 24 | const ( 25 | regexpTypePath regexpType = iota 26 | regexpTypeHost 27 | regexpTypePrefix 28 | regexpTypeQuery 29 | ) 30 | 31 | // newRouteRegexp parses a route template and returns a routeRegexp, 32 | // used to match a host, a path or a query string. 33 | // 34 | // It will extract named variables, assemble a regexp to be matched, create 35 | // a "reverse" template to build URLs and compile regexps to validate variable 36 | // values used in URL building. 37 | // 38 | // Previously we accepted only Python-like identifiers for variable 39 | // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that 40 | // name and pattern can't be empty, and names can't contain a colon. 41 | func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { 42 | // Check if it is well-formed. 43 | idxs, errBraces := braceIndices(tpl) 44 | if errBraces != nil { 45 | return nil, errBraces 46 | } 47 | // Backup the original. 48 | template := tpl 49 | // Now let's parse it. 50 | defaultPattern := "[^/]+" 51 | if typ == regexpTypeQuery { 52 | defaultPattern = ".*" 53 | } else if typ == regexpTypeHost { 54 | defaultPattern = "[^.]+" 55 | } 56 | // Only match strict slash if not matching 57 | if typ != regexpTypePath { 58 | options.strictSlash = false 59 | } 60 | // Set a flag for strictSlash. 61 | endSlash := false 62 | if options.strictSlash && strings.HasSuffix(tpl, "/") { 63 | tpl = tpl[:len(tpl)-1] 64 | endSlash = true 65 | } 66 | varsN := make([]string, len(idxs)/2) 67 | varsR := make([]*regexp.Regexp, len(idxs)/2) 68 | 69 | var pattern, reverse strings.Builder 70 | pattern.WriteByte('^') 71 | 72 | var end, colonIdx, groupIdx int 73 | var err error 74 | var patt, param, name string 75 | for i := 0; i < len(idxs); i += 2 { 76 | // Set all values we are interested in. 77 | groupIdx = i / 2 78 | 79 | raw := tpl[end:idxs[i]] 80 | end = idxs[i+1] 81 | tag := tpl[idxs[i]:end] 82 | 83 | // trim braces from tag 84 | param = tag[1 : len(tag)-1] 85 | 86 | colonIdx = strings.Index(param, ":") 87 | if colonIdx == -1 { 88 | name = param 89 | patt = defaultPattern 90 | } else { 91 | name = param[0:colonIdx] 92 | patt = param[colonIdx+1:] 93 | } 94 | 95 | // Name or pattern can't be empty. 96 | if name == "" || patt == "" { 97 | return nil, fmt.Errorf("mux: missing name or pattern in %q", tag) 98 | } 99 | // Build the regexp pattern. 100 | groupName := varGroupName(groupIdx) 101 | 102 | pattern.WriteString(regexp.QuoteMeta(raw) + "(?P<" + groupName + ">" + patt + ")") 103 | 104 | // Build the reverse template. 105 | reverse.WriteString(raw + "%s") 106 | 107 | // Append variable name and compiled pattern. 108 | varsN[groupIdx] = name 109 | varsR[groupIdx], err = RegexpCompileFunc("^" + patt + "$") 110 | if err != nil { 111 | return nil, fmt.Errorf("mux: error compiling regex for %q: %w", tag, err) 112 | } 113 | } 114 | // Add the remaining. 115 | raw := tpl[end:] 116 | pattern.WriteString(regexp.QuoteMeta(raw)) 117 | if options.strictSlash { 118 | pattern.WriteString("[/]?") 119 | } 120 | if typ == regexpTypeQuery { 121 | // Add the default pattern if the query value is empty 122 | if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { 123 | pattern.WriteString(defaultPattern) 124 | } 125 | } 126 | if typ != regexpTypePrefix { 127 | pattern.WriteByte('$') 128 | } 129 | 130 | // Compile full regexp. 131 | patternStr := pattern.String() 132 | reg, errCompile := RegexpCompileFunc(patternStr) 133 | if errCompile != nil { 134 | return nil, errCompile 135 | } 136 | 137 | // Check for capturing groups which used to work in older versions 138 | if reg.NumSubexp() != len(idxs)/2 { 139 | panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + 140 | "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") 141 | } 142 | 143 | var wildcardHostPort bool 144 | if typ == regexpTypeHost { 145 | if !strings.Contains(patternStr, ":") { 146 | wildcardHostPort = true 147 | } 148 | } 149 | reverse.WriteString(raw) 150 | if endSlash { 151 | reverse.WriteByte('/') 152 | } 153 | 154 | // Done! 155 | return &routeRegexp{ 156 | template: template, 157 | regexpType: typ, 158 | options: options, 159 | regexp: reg, 160 | reverse: reverse.String(), 161 | varsN: varsN, 162 | varsR: varsR, 163 | wildcardHostPort: wildcardHostPort, 164 | }, nil 165 | } 166 | 167 | // routeRegexp stores a regexp to match a host or path and information to 168 | // collect and validate route variables. 169 | type routeRegexp struct { 170 | // The unmodified template. 171 | template string 172 | // The type of match 173 | regexpType regexpType 174 | // Options for matching 175 | options routeRegexpOptions 176 | // Expanded regexp. 177 | regexp *regexp.Regexp 178 | // Reverse template. 179 | reverse string 180 | // Variable names. 181 | varsN []string 182 | // Variable regexps (validators). 183 | varsR []*regexp.Regexp 184 | // Wildcard host-port (no strict port match in hostname) 185 | wildcardHostPort bool 186 | } 187 | 188 | // Match matches the regexp against the URL host or path. 189 | func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { 190 | if r.regexpType == regexpTypeHost { 191 | host := getHost(req) 192 | if r.wildcardHostPort { 193 | // Don't be strict on the port match 194 | if i := strings.Index(host, ":"); i != -1 { 195 | host = host[:i] 196 | } 197 | } 198 | return r.regexp.MatchString(host) 199 | } 200 | 201 | if r.regexpType == regexpTypeQuery { 202 | return r.matchQueryString(req) 203 | } 204 | path := req.URL.Path 205 | if r.options.useEncodedPath { 206 | path = req.URL.EscapedPath() 207 | } 208 | return r.regexp.MatchString(path) 209 | } 210 | 211 | // url builds a URL part using the given values. 212 | func (r *routeRegexp) url(values map[string]string) (string, error) { 213 | urlValues := make([]interface{}, len(r.varsN)) 214 | for k, v := range r.varsN { 215 | value, ok := values[v] 216 | if !ok { 217 | return "", fmt.Errorf("mux: missing route variable %q", v) 218 | } 219 | if r.regexpType == regexpTypeQuery { 220 | value = url.QueryEscape(value) 221 | } 222 | urlValues[k] = value 223 | } 224 | rv := fmt.Sprintf(r.reverse, urlValues...) 225 | if !r.regexp.MatchString(rv) { 226 | // The URL is checked against the full regexp, instead of checking 227 | // individual variables. This is faster but to provide a good error 228 | // message, we check individual regexps if the URL doesn't match. 229 | for k, v := range r.varsN { 230 | if !r.varsR[k].MatchString(values[v]) { 231 | return "", fmt.Errorf( 232 | "mux: variable %q doesn't match, expected %q", values[v], 233 | r.varsR[k].String()) 234 | } 235 | } 236 | } 237 | return rv, nil 238 | } 239 | 240 | // getURLQuery returns a single query parameter from a request URL. 241 | // For a URL with foo=bar&baz=ding, we return only the relevant key 242 | // value pair for the routeRegexp. 243 | func (r *routeRegexp) getURLQuery(req *http.Request) string { 244 | if r.regexpType != regexpTypeQuery { 245 | return "" 246 | } 247 | templateKey := strings.SplitN(r.template, "=", 2)[0] 248 | val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) 249 | if ok { 250 | return templateKey + "=" + val 251 | } 252 | return "" 253 | } 254 | 255 | // findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. 256 | // If key was not found, empty string and false is returned. 257 | func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { 258 | query := []byte(rawQuery) 259 | for len(query) > 0 { 260 | foundKey := query 261 | if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { 262 | foundKey, query = foundKey[:i], foundKey[i+1:] 263 | } else { 264 | query = query[:0] 265 | } 266 | if len(foundKey) == 0 { 267 | continue 268 | } 269 | var value []byte 270 | if i := bytes.IndexByte(foundKey, '='); i >= 0 { 271 | foundKey, value = foundKey[:i], foundKey[i+1:] 272 | } 273 | if len(foundKey) < len(key) { 274 | // Cannot possibly be key. 275 | continue 276 | } 277 | keyString, err := url.QueryUnescape(string(foundKey)) 278 | if err != nil { 279 | continue 280 | } 281 | if keyString != key { 282 | continue 283 | } 284 | valueString, err := url.QueryUnescape(string(value)) 285 | if err != nil { 286 | continue 287 | } 288 | return valueString, true 289 | } 290 | return "", false 291 | } 292 | 293 | func (r *routeRegexp) matchQueryString(req *http.Request) bool { 294 | return r.regexp.MatchString(r.getURLQuery(req)) 295 | } 296 | 297 | // braceIndices returns the first level curly brace indices from a string. 298 | // It returns an error in case of unbalanced braces. 299 | func braceIndices(s string) ([]int, error) { 300 | var level, idx int 301 | var idxs []int 302 | for i := 0; i < len(s); i++ { 303 | switch s[i] { 304 | case '{': 305 | if level++; level == 1 { 306 | idx = i 307 | } 308 | case '}': 309 | if level--; level == 0 { 310 | idxs = append(idxs, idx, i+1) 311 | } else if level < 0 { 312 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 313 | } 314 | } 315 | } 316 | if level != 0 { 317 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 318 | } 319 | return idxs, nil 320 | } 321 | 322 | // varGroupName builds a capturing group name for the indexed variable. 323 | func varGroupName(idx int) string { 324 | return "v" + strconv.Itoa(idx) 325 | } 326 | 327 | // ---------------------------------------------------------------------------- 328 | // routeRegexpGroup 329 | // ---------------------------------------------------------------------------- 330 | 331 | // routeRegexpGroup groups the route matchers that carry variables. 332 | type routeRegexpGroup struct { 333 | host *routeRegexp 334 | path *routeRegexp 335 | queries []*routeRegexp 336 | } 337 | 338 | // setMatch extracts the variables from the URL once a route matches. 339 | func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { 340 | // Store host variables. 341 | if v.host != nil { 342 | if len(v.host.varsN) > 0 { 343 | host := getHost(req) 344 | if v.host.wildcardHostPort { 345 | // Don't be strict on the port match 346 | if i := strings.Index(host, ":"); i != -1 { 347 | host = host[:i] 348 | } 349 | } 350 | matches := v.host.regexp.FindStringSubmatchIndex(host) 351 | if len(matches) > 0 { 352 | m.Vars = extractVars(host, matches, v.host.varsN, m.Vars) 353 | } 354 | } 355 | } 356 | path := req.URL.Path 357 | if r.useEncodedPath { 358 | path = req.URL.EscapedPath() 359 | } 360 | // Store path variables. 361 | if v.path != nil { 362 | if len(v.path.varsN) > 0 { 363 | matches := v.path.regexp.FindStringSubmatchIndex(path) 364 | if len(matches) > 0 { 365 | m.Vars = extractVars(path, matches, v.path.varsN, m.Vars) 366 | } 367 | } 368 | // Check if we should redirect. 369 | if v.path.options.strictSlash { 370 | p1 := strings.HasSuffix(path, "/") 371 | p2 := strings.HasSuffix(v.path.template, "/") 372 | if p1 != p2 { 373 | p := req.URL.Path 374 | if p1 { 375 | p = p[:len(p)-1] 376 | } else { 377 | p += "/" 378 | } 379 | u := replaceURLPath(req.URL, p) 380 | m.Handler = http.RedirectHandler(u, http.StatusMovedPermanently) 381 | } 382 | } 383 | } 384 | // Store query string variables. 385 | for _, q := range v.queries { 386 | if len(q.varsN) > 0 { 387 | queryURL := q.getURLQuery(req) 388 | matches := q.regexp.FindStringSubmatchIndex(queryURL) 389 | if len(matches) > 0 { 390 | m.Vars = extractVars(queryURL, matches, q.varsN, m.Vars) 391 | } 392 | } 393 | } 394 | } 395 | 396 | // getHost tries its best to return the request host. 397 | // According to section 14.23 of RFC 2616 the Host header 398 | // can include the port number if the default value of 80 is not used. 399 | func getHost(r *http.Request) string { 400 | if r.URL.IsAbs() { 401 | return r.URL.Host 402 | } 403 | return r.Host 404 | } 405 | 406 | func extractVars(input string, matches []int, names []string, output map[string]string) map[string]string { 407 | for i, name := range names { 408 | if output == nil { 409 | output = make(map[string]string, len(names)) 410 | } 411 | output[name] = input[matches[2*i+2]:matches[2*i+3]] 412 | } 413 | return output 414 | } 415 | -------------------------------------------------------------------------------- /regexp_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Test_newRouteRegexp_Errors(t *testing.T) { 12 | tests := []struct { 13 | in, out string 14 | }{ 15 | {"/{}", `mux: missing name or pattern in "{}"`}, 16 | {"/{:.}", `mux: missing name or pattern in "{:.}"`}, 17 | {"/{a:}", `mux: missing name or pattern in "{a:}"`}, 18 | {"/{id:abc(}", `mux: error compiling regex for "{id:abc(}":`}, 19 | } 20 | 21 | for _, tc := range tests { 22 | t.Run("Test case for "+tc.in, func(t *testing.T) { 23 | _, err := newRouteRegexp(tc.in, 0, routeRegexpOptions{}) 24 | if err != nil { 25 | if strings.HasPrefix(err.Error(), tc.out) { 26 | return 27 | } 28 | t.Errorf("Resulting error does not contain %q as expected, error: %s", tc.out, err) 29 | } else { 30 | t.Error("Expected error, got nil") 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func Test_findFirstQueryKey(t *testing.T) { 37 | tests := []string{ 38 | "a=1&b=2", 39 | "a=1&a=2&a=banana", 40 | "ascii=%3Ckey%3A+0x90%3E", 41 | "a=1;b=2", 42 | "a=1&a=2;a=banana", 43 | "a==", 44 | "a=%2", 45 | "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", 46 | "a=1& ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;&a=5", 47 | "a=xxxxxxxxxxxxxxxx&b=YYYYYYYYYYYYYYY&c=ppppppppppppppppppp&f=ttttttttttttttttt&a=uuuuuuuuuuuuu", 48 | } 49 | for _, query := range tests { 50 | t.Run(query, func(t *testing.T) { 51 | // Check against url.ParseQuery, ignoring the error. 52 | all, _ := url.ParseQuery(query) 53 | for key, want := range all { 54 | t.Run(key, func(t *testing.T) { 55 | got, ok := findFirstQueryKey(query, key) 56 | if !ok { 57 | t.Error("Did not get expected key", key) 58 | } 59 | if !reflect.DeepEqual(got, want[0]) { 60 | t.Errorf("findFirstQueryKey(%s,%s) = %v, want %v", query, key, got, want[0]) 61 | } 62 | }) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func Benchmark_findQueryKey(b *testing.B) { 69 | tests := []string{ 70 | "a=1&b=2", 71 | "ascii=%3Ckey%3A+0x90%3E", 72 | "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", 73 | "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", 74 | "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", 75 | } 76 | for i, query := range tests { 77 | b.Run(strconv.Itoa(i), func(b *testing.B) { 78 | // Check against url.ParseQuery, ignoring the error. 79 | all, _ := url.ParseQuery(query) 80 | b.ReportAllocs() 81 | b.ResetTimer() 82 | for i := 0; i < b.N; i++ { 83 | for key := range all { 84 | _, _ = findFirstQueryKey(query, key) 85 | } 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func Benchmark_findQueryKeyGoLib(b *testing.B) { 92 | tests := []string{ 93 | "a=1&b=2", 94 | "ascii=%3Ckey%3A+0x90%3E", 95 | "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", 96 | "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", 97 | "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", 98 | } 99 | for i, query := range tests { 100 | b.Run(strconv.Itoa(i), func(b *testing.B) { 101 | // Check against url.ParseQuery, ignoring the error. 102 | all, _ := url.ParseQuery(query) 103 | var u url.URL 104 | u.RawQuery = query 105 | b.ReportAllocs() 106 | b.ResetTimer() 107 | for i := 0; i < b.N; i++ { 108 | for key := range all { 109 | v := u.Query()[key] 110 | if len(v) > 0 { 111 | _ = v[0] 112 | } 113 | } 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // Route stores information to match a request and build URLs. 17 | type Route struct { 18 | // Request handler for the route. 19 | handler http.Handler 20 | // If true, this route never matches: it is only used to build URLs. 21 | buildOnly bool 22 | // The name used to build URLs. 23 | name string 24 | // Error resulted from building a route. 25 | err error 26 | 27 | // The meta data associated with this route 28 | metadata map[any]any 29 | 30 | // "global" reference to all named routes 31 | namedRoutes map[string]*Route 32 | 33 | // route specific middleware 34 | middlewares []middleware 35 | 36 | // config possibly passed in from `Router` 37 | routeConf 38 | } 39 | 40 | // SkipClean reports whether path cleaning is enabled for this route via 41 | // Router.SkipClean. 42 | func (r *Route) SkipClean() bool { 43 | return r.skipClean 44 | } 45 | 46 | // Match matches the route against the request. 47 | func (r *Route) Match(req *http.Request, match *RouteMatch) bool { 48 | if r.buildOnly || r.err != nil { 49 | return false 50 | } 51 | 52 | var matchErr error 53 | 54 | // Match everything. 55 | for _, m := range r.matchers { 56 | if matched := m.Match(req, match); !matched { 57 | if _, ok := m.(methodMatcher); ok { 58 | matchErr = ErrMethodMismatch 59 | continue 60 | } 61 | 62 | // Multiple routes may share the same path but use different HTTP methods. For instance: 63 | // Route 1: POST "/users/{id}". 64 | // Route 2: GET "/users/{id}", parameters: "id": "[0-9]+". 65 | // 66 | // The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2", 67 | // The router should return a "Not Found" error as no route fully matches this request. 68 | if rr, ok := m.(*routeRegexp); ok { 69 | if rr.regexpType == regexpTypeQuery { 70 | matchErr = ErrNotFound 71 | break 72 | } 73 | } 74 | 75 | // Ignore ErrNotFound errors. These errors arise from match call 76 | // to Subrouters. 77 | // 78 | // This prevents subsequent matching subrouters from failing to 79 | // run middleware. If not ignored, the middleware would see a 80 | // non-nil MatchErr and be skipped, even when there was a 81 | // matching route. 82 | if match.MatchErr == ErrNotFound { 83 | match.MatchErr = nil 84 | } 85 | 86 | matchErr = nil // nolint:ineffassign 87 | return false 88 | } 89 | } 90 | 91 | if matchErr != nil { 92 | match.MatchErr = matchErr 93 | return false 94 | } 95 | 96 | if match.MatchErr != nil && r.handler != nil { 97 | // We found a route which matches request method, clear MatchErr 98 | match.MatchErr = nil 99 | // Then override the mis-matched handler 100 | match.Handler = r.handler 101 | } 102 | 103 | // Yay, we have a match. Let's collect some info about it. 104 | if match.Route == nil { 105 | match.Route = r 106 | } 107 | if match.Handler == nil { 108 | match.Handler = r.GetHandlerWithMiddlewares() 109 | } 110 | 111 | // Set variables. 112 | r.regexp.setMatch(req, match, r) 113 | return true 114 | } 115 | 116 | // ---------------------------------------------------------------------------- 117 | // Route attributes 118 | // ---------------------------------------------------------------------------- 119 | 120 | // GetError returns an error resulted from building the route, if any. 121 | func (r *Route) GetError() error { 122 | return r.err 123 | } 124 | 125 | // BuildOnly sets the route to never match: it is only used to build URLs. 126 | func (r *Route) BuildOnly() *Route { 127 | r.buildOnly = true 128 | return r 129 | } 130 | 131 | // MetaData ------------------------------------------------------------------- 132 | 133 | // Metadata is used to set metadata on a route 134 | func (r *Route) Metadata(key any, value any) *Route { 135 | if r.metadata == nil { 136 | r.metadata = make(map[any]any) 137 | } 138 | 139 | r.metadata[key] = value 140 | return r 141 | } 142 | 143 | // GetMetadata returns the metadata map for route 144 | func (r *Route) GetMetadata() map[any]any { 145 | return r.metadata 146 | } 147 | 148 | // MetadataContains returns whether or not the key is present in the metadata map 149 | func (r *Route) MetadataContains(key any) bool { 150 | _, ok := r.metadata[key] 151 | return ok 152 | } 153 | 154 | // GetMetadataValue returns the value of a specific key in the metadata map. If the key is not present in the map mux.ErrMetadataKeyNotFound is returned 155 | func (r *Route) GetMetadataValue(key any) (any, error) { 156 | value, ok := r.metadata[key] 157 | if !ok { 158 | return nil, ErrMetadataKeyNotFound 159 | } 160 | 161 | return value, nil 162 | } 163 | 164 | // GetMetadataValueOr returns the value of a specific key in the metadata map. If the key is not present in the metadata the fallback value is returned 165 | func (r *Route) GetMetadataValueOr(key any, fallbackValue any) any { 166 | value, ok := r.metadata[key] 167 | if !ok { 168 | return fallbackValue 169 | } 170 | 171 | return value 172 | } 173 | 174 | // Handler -------------------------------------------------------------------- 175 | 176 | // Handler sets a handler for the route. 177 | func (r *Route) Handler(handler http.Handler) *Route { 178 | if r.err == nil { 179 | r.handler = handler 180 | } 181 | return r 182 | } 183 | 184 | // HandlerFunc sets a handler function for the route. 185 | func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { 186 | return r.Handler(http.HandlerFunc(f)) 187 | } 188 | 189 | // GetHandler returns the handler for the route, if any. 190 | func (r *Route) GetHandler() http.Handler { 191 | return r.handler 192 | } 193 | 194 | // GetHandlerWithMiddleware returns the route handler wrapped in the assigned middlewares. 195 | // If no middlewares are specified, just the handler, if any, is returned. 196 | func (r *Route) GetHandlerWithMiddlewares() http.Handler { 197 | handler := r.handler 198 | 199 | if handler != nil && len(r.middlewares) > 0 { 200 | for i := len(r.middlewares) - 1; i >= 0; i-- { 201 | handler = r.middlewares[i].Middleware(handler) 202 | } 203 | } 204 | 205 | return handler 206 | } 207 | 208 | // Name ----------------------------------------------------------------------- 209 | 210 | // Name sets the name for the route, used to build URLs. 211 | // It is an error to call Name more than once on a route. 212 | func (r *Route) Name(name string) *Route { 213 | if r.name != "" { 214 | r.err = fmt.Errorf("mux: route already has name %q, can't set %q", 215 | r.name, name) 216 | } 217 | if r.err == nil { 218 | r.name = name 219 | r.namedRoutes[name] = r 220 | } 221 | return r 222 | } 223 | 224 | // GetName returns the name for the route, if any. 225 | func (r *Route) GetName() string { 226 | return r.name 227 | } 228 | 229 | // ---------------------------------------------------------------------------- 230 | // Matchers 231 | // ---------------------------------------------------------------------------- 232 | 233 | // matcher types try to match a request. 234 | type matcher interface { 235 | Match(*http.Request, *RouteMatch) bool 236 | } 237 | 238 | // addMatcher adds a matcher to the route. 239 | func (r *Route) addMatcher(m matcher) *Route { 240 | if r.err == nil { 241 | r.matchers = append(r.matchers, m) 242 | } 243 | return r 244 | } 245 | 246 | // addRegexpMatcher adds a host or path matcher and builder to a route. 247 | func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { 248 | if r.err != nil { 249 | return r.err 250 | } 251 | if typ == regexpTypePath || typ == regexpTypePrefix { 252 | if len(tpl) > 0 && tpl[0] != '/' { 253 | return fmt.Errorf("mux: path must start with a slash, got %q", tpl) 254 | } 255 | if r.regexp.path != nil { 256 | tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl 257 | } 258 | } 259 | rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ 260 | strictSlash: r.strictSlash, 261 | useEncodedPath: r.useEncodedPath, 262 | }) 263 | if err != nil { 264 | return err 265 | } 266 | for _, q := range r.regexp.queries { 267 | if err = uniqueVars(rr.varsN, q.varsN); err != nil { 268 | return err 269 | } 270 | } 271 | if typ == regexpTypeHost { 272 | if r.regexp.path != nil { 273 | if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { 274 | return err 275 | } 276 | } 277 | r.regexp.host = rr 278 | } else { 279 | if r.regexp.host != nil { 280 | if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { 281 | return err 282 | } 283 | } 284 | if typ == regexpTypeQuery { 285 | r.regexp.queries = append(r.regexp.queries, rr) 286 | } else { 287 | r.regexp.path = rr 288 | } 289 | } 290 | r.addMatcher(rr) 291 | return nil 292 | } 293 | 294 | // Headers -------------------------------------------------------------------- 295 | 296 | // headerMatcher matches the request against header values. 297 | type headerMatcher map[string]string 298 | 299 | func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { 300 | return matchMapWithString(m, r.Header, true) 301 | } 302 | 303 | // Headers adds a matcher for request header values. 304 | // It accepts a sequence of key/value pairs to be matched. For example: 305 | // 306 | // r := mux.NewRouter().NewRoute() 307 | // r.Headers("Content-Type", "application/json", 308 | // "X-Requested-With", "XMLHttpRequest") 309 | // 310 | // The above route will only match if both request header values match. 311 | // If the value is an empty string, it will match any value if the key is set. 312 | func (r *Route) Headers(pairs ...string) *Route { 313 | if r.err == nil { 314 | var headers map[string]string 315 | headers, r.err = mapFromPairsToString(pairs...) 316 | return r.addMatcher(headerMatcher(headers)) 317 | } 318 | return r 319 | } 320 | 321 | // headerRegexMatcher matches the request against the route given a regex for the header 322 | type headerRegexMatcher map[string]*regexp.Regexp 323 | 324 | func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { 325 | return matchMapWithRegex(m, r.Header, true) 326 | } 327 | 328 | // HeadersRegexp accepts a sequence of key/value pairs, where the value has regex 329 | // support. For example: 330 | // 331 | // r := mux.NewRouter().NewRoute() 332 | // r.HeadersRegexp("Content-Type", "application/(text|json)", 333 | // "X-Requested-With", "XMLHttpRequest") 334 | // 335 | // The above route will only match if both the request header matches both regular expressions. 336 | // If the value is an empty string, it will match any value if the key is set. 337 | // Use the start and end of string anchors (^ and $) to match an exact value. 338 | func (r *Route) HeadersRegexp(pairs ...string) *Route { 339 | if r.err == nil { 340 | var headers map[string]*regexp.Regexp 341 | headers, r.err = mapFromPairsToRegex(pairs...) 342 | return r.addMatcher(headerRegexMatcher(headers)) 343 | } 344 | return r 345 | } 346 | 347 | // Host ----------------------------------------------------------------------- 348 | 349 | // Host adds a matcher for the URL host. 350 | // It accepts a template with zero or more URL variables enclosed by {}. 351 | // Variables can define an optional regexp pattern to be matched: 352 | // 353 | // - {name} matches anything until the next dot. 354 | // 355 | // - {name:pattern} matches the given regexp pattern. 356 | // 357 | // For example: 358 | // 359 | // r := mux.NewRouter().NewRoute() 360 | // r.Host("www.example.com") 361 | // r.Host("{subdomain}.domain.com") 362 | // r.Host("{subdomain:[a-z]+}.domain.com") 363 | // 364 | // Variable names must be unique in a given route. They can be retrieved 365 | // calling mux.Vars(request). 366 | func (r *Route) Host(tpl string) *Route { 367 | r.err = r.addRegexpMatcher(tpl, regexpTypeHost) 368 | return r 369 | } 370 | 371 | // MatcherFunc ---------------------------------------------------------------- 372 | 373 | // MatcherFunc is the function signature used by custom matchers. 374 | type MatcherFunc func(*http.Request, *RouteMatch) bool 375 | 376 | // Match returns the match for a given request. 377 | func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { 378 | return m(r, match) 379 | } 380 | 381 | // MatcherFunc adds a custom function to be used as request matcher. 382 | func (r *Route) MatcherFunc(f MatcherFunc) *Route { 383 | return r.addMatcher(f) 384 | } 385 | 386 | // Methods -------------------------------------------------------------------- 387 | 388 | // methodMatcher matches the request against HTTP methods. 389 | type methodMatcher []string 390 | 391 | func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { 392 | return matchInArray(m, r.Method) 393 | } 394 | 395 | // Methods adds a matcher for HTTP methods. 396 | // It accepts a sequence of one or more methods to be matched, e.g.: 397 | // "GET", "POST", "PUT". 398 | func (r *Route) Methods(methods ...string) *Route { 399 | for k, v := range methods { 400 | methods[k] = strings.ToUpper(v) 401 | } 402 | return r.addMatcher(methodMatcher(methods)) 403 | } 404 | 405 | // Path ----------------------------------------------------------------------- 406 | 407 | // Path adds a matcher for the URL path. 408 | // It accepts a template with zero or more URL variables enclosed by {}. The 409 | // template must start with a "/". 410 | // Variables can define an optional regexp pattern to be matched: 411 | // 412 | // - {name} matches anything until the next slash. 413 | // 414 | // - {name:pattern} matches the given regexp pattern. 415 | // 416 | // For example: 417 | // 418 | // r := mux.NewRouter().NewRoute() 419 | // r.Path("/products/").Handler(ProductsHandler) 420 | // r.Path("/products/{key}").Handler(ProductsHandler) 421 | // r.Path("/articles/{category}/{id:[0-9]+}"). 422 | // Handler(ArticleHandler) 423 | // 424 | // Variable names must be unique in a given route. They can be retrieved 425 | // calling mux.Vars(request). 426 | func (r *Route) Path(tpl string) *Route { 427 | r.err = r.addRegexpMatcher(tpl, regexpTypePath) 428 | return r 429 | } 430 | 431 | // PathPrefix ----------------------------------------------------------------- 432 | 433 | // PathPrefix adds a matcher for the URL path prefix. This matches if the given 434 | // template is a prefix of the full URL path. See Route.Path() for details on 435 | // the tpl argument. 436 | // 437 | // Note that it does not treat slashes specially ("/foobar/" will be matched by 438 | // the prefix "/foo") so you may want to use a trailing slash here. 439 | // 440 | // Also note that the setting of Router.StrictSlash() has no effect on routes 441 | // with a PathPrefix matcher. 442 | func (r *Route) PathPrefix(tpl string) *Route { 443 | r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) 444 | return r 445 | } 446 | 447 | // Query ---------------------------------------------------------------------- 448 | 449 | // Queries adds a matcher for URL query values. 450 | // It accepts a sequence of key/value pairs. Values may define variables. 451 | // For example: 452 | // 453 | // r := mux.NewRouter().NewRoute() 454 | // r.Queries("foo", "bar", "id", "{id:[0-9]+}") 455 | // 456 | // The above route will only match if the URL contains the defined queries 457 | // values, e.g.: ?foo=bar&id=42. 458 | // 459 | // If the value is an empty string, it will match any value if the key is set. 460 | // 461 | // Variables can define an optional regexp pattern to be matched: 462 | // 463 | // - {name} matches anything until the next slash. 464 | // 465 | // - {name:pattern} matches the given regexp pattern. 466 | func (r *Route) Queries(pairs ...string) *Route { 467 | length := len(pairs) 468 | if length%2 != 0 { 469 | r.err = fmt.Errorf( 470 | "mux: number of parameters must be multiple of 2, got %v", pairs) 471 | return nil 472 | } 473 | for i := 0; i < length; i += 2 { 474 | if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { 475 | return r 476 | } 477 | } 478 | 479 | return r 480 | } 481 | 482 | // Schemes -------------------------------------------------------------------- 483 | 484 | // schemeMatcher matches the request against URL schemes. 485 | type schemeMatcher []string 486 | 487 | func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { 488 | scheme := r.URL.Scheme 489 | // https://golang.org/pkg/net/http/#Request 490 | // "For [most] server requests, fields other than Path and RawQuery will be 491 | // empty." 492 | // Since we're an http muxer, the scheme is either going to be http or https 493 | // though, so we can just set it based on the tls termination state. 494 | if scheme == "" { 495 | if r.TLS == nil { 496 | scheme = "http" 497 | } else { 498 | scheme = "https" 499 | } 500 | } 501 | return matchInArray(m, scheme) 502 | } 503 | 504 | // Schemes adds a matcher for URL schemes. 505 | // It accepts a sequence of schemes to be matched, e.g.: "http", "https". 506 | // If the request's URL has a scheme set, it will be matched against. 507 | // Generally, the URL scheme will only be set if a previous handler set it, 508 | // such as the ProxyHeaders handler from gorilla/handlers. 509 | // If unset, the scheme will be determined based on the request's TLS 510 | // termination state. 511 | // The first argument to Schemes will be used when constructing a route URL. 512 | func (r *Route) Schemes(schemes ...string) *Route { 513 | for k, v := range schemes { 514 | schemes[k] = strings.ToLower(v) 515 | } 516 | if len(schemes) > 0 { 517 | r.buildScheme = schemes[0] 518 | } 519 | return r.addMatcher(schemeMatcher(schemes)) 520 | } 521 | 522 | // BuildVarsFunc -------------------------------------------------------------- 523 | 524 | // BuildVarsFunc is the function signature used by custom build variable 525 | // functions (which can modify route variables before a route's URL is built). 526 | type BuildVarsFunc func(map[string]string) map[string]string 527 | 528 | // BuildVarsFunc adds a custom function to be used to modify build variables 529 | // before a route's URL is built. 530 | func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { 531 | if r.buildVarsFunc != nil { 532 | // compose the old and new functions 533 | old := r.buildVarsFunc 534 | r.buildVarsFunc = func(m map[string]string) map[string]string { 535 | return f(old(m)) 536 | } 537 | } else { 538 | r.buildVarsFunc = f 539 | } 540 | return r 541 | } 542 | 543 | // Subrouter ------------------------------------------------------------------ 544 | 545 | // Subrouter creates a subrouter for the route. 546 | // 547 | // It will test the inner routes only if the parent route matched. For example: 548 | // 549 | // r := mux.NewRouter().NewRoute() 550 | // s := r.Host("www.example.com").Subrouter() 551 | // s.HandleFunc("/products/", ProductsHandler) 552 | // s.HandleFunc("/products/{key}", ProductHandler) 553 | // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 554 | // 555 | // Here, the routes registered in the subrouter won't be tested if the host 556 | // doesn't match. 557 | func (r *Route) Subrouter() *Router { 558 | // initialize a subrouter with a copy of the parent route's configuration 559 | router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} 560 | r.addMatcher(router) 561 | return router 562 | } 563 | 564 | // ---------------------------------------------------------------------------- 565 | // URL building 566 | // ---------------------------------------------------------------------------- 567 | 568 | // URL builds a URL for the route. 569 | // 570 | // It accepts a sequence of key/value pairs for the route variables. For 571 | // example, given this route: 572 | // 573 | // r := mux.NewRouter() 574 | // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 575 | // Name("article") 576 | // 577 | // ...a URL for it can be built using: 578 | // 579 | // url, err := r.Get("article").URL("category", "technology", "id", "42") 580 | // 581 | // ...which will return an url.URL with the following path: 582 | // 583 | // "/articles/technology/42" 584 | // 585 | // This also works for host variables: 586 | // 587 | // r := mux.NewRouter() 588 | // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 589 | // Host("{subdomain}.domain.com"). 590 | // Name("article") 591 | // 592 | // // url.String() will be "http://news.domain.com/articles/technology/42" 593 | // url, err := r.Get("article").URL("subdomain", "news", 594 | // "category", "technology", 595 | // "id", "42") 596 | // 597 | // The scheme of the resulting url will be the first argument that was passed to Schemes: 598 | // 599 | // // url.String() will be "https://example.com" 600 | // r := mux.NewRouter().NewRoute() 601 | // url, err := r.Host("example.com") 602 | // .Schemes("https", "http").URL() 603 | // 604 | // All variables defined in the route are required, and their values must 605 | // conform to the corresponding patterns. 606 | func (r *Route) URL(pairs ...string) (*url.URL, error) { 607 | if r.err != nil { 608 | return nil, r.err 609 | } 610 | values, err := r.prepareVars(pairs...) 611 | if err != nil { 612 | return nil, err 613 | } 614 | var scheme, host, path string 615 | queries := make([]string, 0, len(r.regexp.queries)) 616 | if r.regexp.host != nil { 617 | if host, err = r.regexp.host.url(values); err != nil { 618 | return nil, err 619 | } 620 | scheme = "http" 621 | if r.buildScheme != "" { 622 | scheme = r.buildScheme 623 | } 624 | } 625 | if r.regexp.path != nil { 626 | if path, err = r.regexp.path.url(values); err != nil { 627 | return nil, err 628 | } 629 | } 630 | for _, q := range r.regexp.queries { 631 | var query string 632 | if query, err = q.url(values); err != nil { 633 | return nil, err 634 | } 635 | queries = append(queries, query) 636 | } 637 | return &url.URL{ 638 | Scheme: scheme, 639 | Host: host, 640 | Path: path, 641 | RawQuery: strings.Join(queries, "&"), 642 | }, nil 643 | } 644 | 645 | // URLHost builds the host part of the URL for a route. See Route.URL(). 646 | // 647 | // The route must have a host defined. 648 | func (r *Route) URLHost(pairs ...string) (*url.URL, error) { 649 | if r.err != nil { 650 | return nil, r.err 651 | } 652 | if r.regexp.host == nil { 653 | return nil, errors.New("mux: route doesn't have a host") 654 | } 655 | values, err := r.prepareVars(pairs...) 656 | if err != nil { 657 | return nil, err 658 | } 659 | host, err := r.regexp.host.url(values) 660 | if err != nil { 661 | return nil, err 662 | } 663 | u := &url.URL{ 664 | Scheme: "http", 665 | Host: host, 666 | } 667 | if r.buildScheme != "" { 668 | u.Scheme = r.buildScheme 669 | } 670 | return u, nil 671 | } 672 | 673 | // URLPath builds the path part of the URL for a route. See Route.URL(). 674 | // 675 | // The route must have a path defined. 676 | func (r *Route) URLPath(pairs ...string) (*url.URL, error) { 677 | if r.err != nil { 678 | return nil, r.err 679 | } 680 | if r.regexp.path == nil { 681 | return nil, errors.New("mux: route doesn't have a path") 682 | } 683 | values, err := r.prepareVars(pairs...) 684 | if err != nil { 685 | return nil, err 686 | } 687 | path, err := r.regexp.path.url(values) 688 | if err != nil { 689 | return nil, err 690 | } 691 | return &url.URL{ 692 | Path: path, 693 | }, nil 694 | } 695 | 696 | // GetPathTemplate returns the template used to build the 697 | // route match. 698 | // This is useful for building simple REST API documentation and for instrumentation 699 | // against third-party services. 700 | // An error will be returned if the route does not define a path. 701 | func (r *Route) GetPathTemplate() (string, error) { 702 | if r.err != nil { 703 | return "", r.err 704 | } 705 | if r.regexp.path == nil { 706 | return "", errors.New("mux: route doesn't have a path") 707 | } 708 | return r.regexp.path.template, nil 709 | } 710 | 711 | // GetPathRegexp returns the expanded regular expression used to match route path. 712 | // This is useful for building simple REST API documentation and for instrumentation 713 | // against third-party services. 714 | // An error will be returned if the route does not define a path. 715 | func (r *Route) GetPathRegexp() (string, error) { 716 | if r.err != nil { 717 | return "", r.err 718 | } 719 | if r.regexp.path == nil { 720 | return "", errors.New("mux: route does not have a path") 721 | } 722 | return r.regexp.path.regexp.String(), nil 723 | } 724 | 725 | // GetQueriesRegexp returns the expanded regular expressions used to match the 726 | // route queries. 727 | // This is useful for building simple REST API documentation and for instrumentation 728 | // against third-party services. 729 | // An error will be returned if the route does not have queries. 730 | func (r *Route) GetQueriesRegexp() ([]string, error) { 731 | if r.err != nil { 732 | return nil, r.err 733 | } 734 | if r.regexp.queries == nil { 735 | return nil, errors.New("mux: route doesn't have queries") 736 | } 737 | queries := make([]string, 0, len(r.regexp.queries)) 738 | for _, query := range r.regexp.queries { 739 | queries = append(queries, query.regexp.String()) 740 | } 741 | return queries, nil 742 | } 743 | 744 | // GetQueriesTemplates returns the templates used to build the 745 | // query matching. 746 | // This is useful for building simple REST API documentation and for instrumentation 747 | // against third-party services. 748 | // An error will be returned if the route does not define queries. 749 | func (r *Route) GetQueriesTemplates() ([]string, error) { 750 | if r.err != nil { 751 | return nil, r.err 752 | } 753 | if r.regexp.queries == nil { 754 | return nil, errors.New("mux: route doesn't have queries") 755 | } 756 | queries := make([]string, 0, len(r.regexp.queries)) 757 | for _, query := range r.regexp.queries { 758 | queries = append(queries, query.template) 759 | } 760 | return queries, nil 761 | } 762 | 763 | // GetMethods returns the methods the route matches against 764 | // This is useful for building simple REST API documentation and for instrumentation 765 | // against third-party services. 766 | // An error will be returned if route does not have methods. 767 | func (r *Route) GetMethods() ([]string, error) { 768 | if r.err != nil { 769 | return nil, r.err 770 | } 771 | for _, m := range r.matchers { 772 | if methods, ok := m.(methodMatcher); ok { 773 | return []string(methods), nil 774 | } 775 | } 776 | return nil, errors.New("mux: route doesn't have methods") 777 | } 778 | 779 | // GetHostTemplate returns the template used to build the 780 | // route match. 781 | // This is useful for building simple REST API documentation and for instrumentation 782 | // against third-party services. 783 | // An error will be returned if the route does not define a host. 784 | func (r *Route) GetHostTemplate() (string, error) { 785 | if r.err != nil { 786 | return "", r.err 787 | } 788 | if r.regexp.host == nil { 789 | return "", errors.New("mux: route doesn't have a host") 790 | } 791 | return r.regexp.host.template, nil 792 | } 793 | 794 | // GetVarNames returns the names of all variables added by regexp matchers 795 | // These can be used to know which route variables should be passed into r.URL() 796 | func (r *Route) GetVarNames() ([]string, error) { 797 | if r.err != nil { 798 | return nil, r.err 799 | } 800 | var varNames []string 801 | if r.regexp.host != nil { 802 | varNames = append(varNames, r.regexp.host.varsN...) 803 | } 804 | if r.regexp.path != nil { 805 | varNames = append(varNames, r.regexp.path.varsN...) 806 | } 807 | for _, regx := range r.regexp.queries { 808 | varNames = append(varNames, regx.varsN...) 809 | } 810 | return varNames, nil 811 | } 812 | 813 | // prepareVars converts the route variable pairs into a map. If the route has a 814 | // BuildVarsFunc, it is invoked. 815 | func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { 816 | m, err := mapFromPairsToString(pairs...) 817 | if err != nil { 818 | return nil, err 819 | } 820 | return r.buildVars(m), nil 821 | } 822 | 823 | func (r *Route) buildVars(m map[string]string) map[string]string { 824 | if r.buildVarsFunc != nil { 825 | m = r.buildVarsFunc(m) 826 | } 827 | return m 828 | } 829 | -------------------------------------------------------------------------------- /route_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "reflect" 7 | "regexp" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | var testNewRouterMu sync.Mutex 13 | var testHandler = http.NotFoundHandler() 14 | 15 | func BenchmarkNewRouter(b *testing.B) { 16 | testNewRouterMu.Lock() 17 | defer testNewRouterMu.Unlock() 18 | 19 | // Set the RegexpCompileFunc to the default regexp.Compile. 20 | RegexpCompileFunc = regexp.Compile 21 | 22 | b.ReportAllocs() 23 | b.ResetTimer() 24 | 25 | for i := 0; i < b.N; i++ { 26 | testNewRouter(b, testHandler) 27 | } 28 | } 29 | 30 | func BenchmarkNewRouterRegexpFunc(b *testing.B) { 31 | testNewRouterMu.Lock() 32 | defer testNewRouterMu.Unlock() 33 | 34 | // We preallocate the size to 8. 35 | cache := make(map[string]*regexp.Regexp, 8) 36 | 37 | // Override the RegexpCompileFunc to reuse compiled expressions 38 | // from the `cache` map. Real world caches should have eviction 39 | // policies or some sort of approach to limit memory use. 40 | RegexpCompileFunc = func(expr string) (*regexp.Regexp, error) { 41 | if regex, ok := cache[expr]; ok { 42 | return regex, nil 43 | } 44 | 45 | regex, err := regexp.Compile(expr) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | cache[expr] = regex 51 | return regex, nil 52 | } 53 | 54 | b.ReportAllocs() 55 | b.ResetTimer() 56 | 57 | for i := 0; i < b.N; i++ { 58 | testNewRouter(b, testHandler) 59 | } 60 | } 61 | 62 | func testNewRouter(_ testing.TB, handler http.Handler) { 63 | r := NewRouter() 64 | // A route with a route variable: 65 | r.Handle("/metrics/{type}", handler) 66 | r.Queries("orgID", "{orgID:[0-9]*?}") 67 | r.Host("{subdomain}.domain.com") 68 | } 69 | 70 | func TestRouteMetadata(t *testing.T) { 71 | router := NewRouter() 72 | rw := NewRecorder() 73 | 74 | expectedMap := make(map[any]any) 75 | expectedMap["key"] = "value" 76 | 77 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 78 | route := CurrentRoute(r) 79 | metadata := route.GetMetadata() 80 | 81 | if !reflect.DeepEqual(metadata, expectedMap) { 82 | println(metadata) 83 | t.Fatalf("Expected map does not equal the metadata map") 84 | } 85 | 86 | }).Metadata("key", "value") 87 | 88 | router.HandleFunc("/single-value", func(w http.ResponseWriter, r *http.Request) { 89 | route := CurrentRoute(r) 90 | value, err := route.GetMetadataValue("key") 91 | if err != nil { 92 | t.Fatalf("Expected metadata value to be present, but gave error: %s", err) 93 | } 94 | 95 | stringValue, ok := value.(string) 96 | if !ok { 97 | t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value)) 98 | } 99 | 100 | if stringValue != "value" { 101 | t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value", stringValue) 102 | } 103 | 104 | _, err = route.GetMetadataValue("key2") 105 | if err == nil { 106 | t.Fatalf("Expected metadata key not to be present and error, but error was nil") 107 | } 108 | 109 | if !errors.Is(err, ErrMetadataKeyNotFound) { 110 | t.Fatalf("Expected error to be ErrMetadataKeyNotFound but got: %s", err) 111 | } 112 | 113 | }).Metadata("key", "value") 114 | 115 | router.HandleFunc("/single-value-fallback", func(w http.ResponseWriter, r *http.Request) { 116 | route := CurrentRoute(r) 117 | value := route.GetMetadataValueOr("key", "value-fallback") 118 | 119 | stringValue, ok := value.(string) 120 | if !ok { 121 | t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value)) 122 | } 123 | 124 | if stringValue != "value" { 125 | t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value", stringValue) 126 | } 127 | 128 | fallbackValue := route.GetMetadataValueOr("key2", "value2") 129 | fallbackStringValue, ok := fallbackValue.(string) 130 | if !ok { 131 | t.Fatalf("Expected metadata value to be string, but was: %s", reflect.TypeOf(value)) 132 | } 133 | 134 | if fallbackStringValue != "value2" { 135 | t.Fatalf("Expected metadata value to be '%s', but got '%s'", "value2", fallbackStringValue) 136 | } 137 | 138 | }).Metadata("key", "value") 139 | 140 | t.Run("get metadata map", func(t *testing.T) { 141 | req := newRequest("GET", "/") 142 | router.ServeHTTP(rw, req) 143 | }) 144 | 145 | t.Run("get metadata value", func(t *testing.T) { 146 | req := newRequest("GET", "/single-value") 147 | router.ServeHTTP(rw, req) 148 | }) 149 | 150 | t.Run("get metadata value or fallback", func(t *testing.T) { 151 | req := newRequest("GET", "/single-value-fallback") 152 | router.ServeHTTP(rw, req) 153 | }) 154 | } 155 | -------------------------------------------------------------------------------- /test_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import "net/http" 8 | 9 | // SetURLVars sets the URL variables for the given request, to be accessed via 10 | // mux.Vars for testing route behaviour. Arguments are not modified, a shallow 11 | // copy is returned. 12 | // 13 | // This API should only be used for testing purposes; it provides a way to 14 | // inject variables into the request context. Alternatively, URL variables 15 | // can be set by making a route that captures the required variables, 16 | // starting a server and sending the request to that server. 17 | func SetURLVars(r *http.Request, val map[string]string) *http.Request { 18 | return requestWithVars(r, val) 19 | } 20 | --------------------------------------------------------------------------------