├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── path.go ├── path_test.go ├── router.go ├── router_test.go ├── tree.go └── tree_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.7.x 5 | - 1.8.x 6 | - 1.9.x 7 | - 1.10.x 8 | - 1.11.x 9 | - 1.12.x 10 | - 1.13.x 11 | - master 12 | matrix: 13 | allow_failures: 14 | - go: master 15 | fast_finish: true 16 | before_install: 17 | - go get github.com/mattn/goveralls 18 | script: 19 | - go test -v -covermode=count -coverprofile=coverage.out 20 | - go vet ./... 21 | - test -z "$(gofmt -d -s . | tee /dev/stderr)" 22 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2013, Julien Schmidt 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HttpRouter [![Coverage Status](https://coveralls.io/repos/github/julienschmidt/httprouter/badge.svg?branch=master)](https://coveralls.io/github/julienschmidt/httprouter?branch=master) [![Docs](https://godoc.org/github.com/julienschmidt/httprouter?status.svg)](http://pkg.go.dev/github.com/julienschmidt/httprouter) 2 | 3 | HttpRouter is a lightweight high performance HTTP request router (also called *multiplexer* or just *mux* for short) for [Go](https://golang.org/). 4 | 5 | In contrast to the [default mux](https://golang.org/pkg/net/http/#ServeMux) of Go's `net/http` package, this router supports variables in the routing pattern and matches against the request method. It also scales better. 6 | 7 | The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching. 8 | 9 | ## Features 10 | 11 | **Only explicit matches:** With other routers, like [`http.ServeMux`](https://golang.org/pkg/net/http/#ServeMux), a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like *longest match* or *first registered, first matched*. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience. 12 | 13 | **Stop caring about trailing slashes:** Choose the URL style you like, the router automatically redirects the client if a trailing slash is missing or if there is one extra. Of course it only does so, if the new path has a handler. If you don't like it, you can [turn off this behavior](https://godoc.org/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash). 14 | 15 | **Path auto-correction:** Besides detecting the missing or additional trailing slash at no extra cost, the router can also fix wrong cases and remove superfluous path elements (like `../` or `//`). Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? HttpRouter can help him by making a case-insensitive look-up and redirecting him to the correct URL. 16 | 17 | **Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap. 18 | 19 | **Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. The only heap allocations that are made are building the slice of the key-value pairs for path parameters, and building new context and request objects (the latter only in the standard `Handler`/`HandlerFunc` API). In the 3-argument API, if the request path contains no parameters not a single heap allocation is necessary. 20 | 21 | **Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). See below for technical details of the implementation. 22 | 23 | **No more server crashes:** You can set a [Panic handler](https://godoc.org/github.com/julienschmidt/httprouter#Router.PanicHandler) to deal with panics occurring during handling a HTTP request. The router then recovers and lets the `PanicHandler` log what happened and deliver a nice error page. 24 | 25 | **Perfect for APIs:** The router design encourages to build sensible, hierarchical RESTful APIs. Moreover it has built-in native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html) and `405 Method Not Allowed` replies. 26 | 27 | Of course you can also set **custom [`NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) and [`MethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](https://godoc.org/github.com/julienschmidt/httprouter#Router.ServeFiles). 28 | 29 | ## Usage 30 | 31 | This is just a quick introduction, view the [Docs](http://pkg.go.dev/github.com/julienschmidt/httprouter) for details. 32 | 33 | $ go get github.com/julienschmidt/httprouter 34 | 35 | and use it, like in this trivial example: 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | "net/http" 43 | "log" 44 | 45 | "github.com/julienschmidt/httprouter" 46 | ) 47 | 48 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 49 | fmt.Fprint(w, "Welcome!\n") 50 | } 51 | 52 | func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 53 | fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 54 | } 55 | 56 | func main() { 57 | router := httprouter.New() 58 | router.GET("/", Index) 59 | router.GET("/hello/:name", Hello) 60 | 61 | log.Fatal(http.ListenAndServe(":8080", router)) 62 | } 63 | ``` 64 | 65 | ### Named parameters 66 | 67 | As you can see, `:name` is a *named parameter*. The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s. You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method: `:name` can be retrieved by `ByName("name")`. 68 | 69 | When using a `http.Handler` (using `router.Handler` or `http.HandlerFunc`) instead of HttpRouter's handle API using a 3rd function parameter, the named parameters are stored in the `request.Context`. See more below under [Why doesn't this work with http.Handler?](#why-doesnt-this-work-with-httphandler). 70 | 71 | Named parameters only match a single path segment: 72 | 73 | ``` 74 | Pattern: /user/:user 75 | 76 | /user/gordon match 77 | /user/you match 78 | /user/gordon/profile no match 79 | /user/ no match 80 | ``` 81 | 82 | **Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other. 83 | 84 | ### Catch-All parameters 85 | 86 | The second type are *catch-all* parameters and have the form `*name`. Like the name suggests, they match everything. Therefore they must always be at the **end** of the pattern: 87 | 88 | ``` 89 | Pattern: /src/*filepath 90 | 91 | /src/ match 92 | /src/somefile.go match 93 | /src/subdir/somefile.go match 94 | ``` 95 | 96 | ## How does it work? 97 | 98 | The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like: 99 | 100 | ``` 101 | Priority Path Handle 102 | 9 \ *<1> 103 | 3 ├s nil 104 | 2 |├earch\ *<2> 105 | 1 |└upport\ *<3> 106 | 2 ├blog\ *<4> 107 | 1 | └:post nil 108 | 1 | └\ *<5> 109 | 2 ├about-us\ *<6> 110 | 1 | └team\ *<7> 111 | 1 └contact\ *<8> 112 | ``` 113 | 114 | Every `*` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark), this works very well and efficient. 115 | 116 | Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, it also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree. 117 | 118 | For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways: 119 | 120 | 1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible. 121 | 2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right. 122 | 123 | ``` 124 | ├------------ 125 | ├--------- 126 | ├----- 127 | ├---- 128 | ├-- 129 | ├-- 130 | └- 131 | ``` 132 | 133 | ## Why doesn't this work with `http.Handler`? 134 | 135 | **It does!** The router itself implements the `http.Handler` interface. Moreover the router provides convenient [adapters for `http.Handler`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [`http.HandlerFunc`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s which allows them to be used as a [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route. 136 | 137 | Named parameters can be accessed `request.Context`: 138 | 139 | ```go 140 | func Hello(w http.ResponseWriter, r *http.Request) { 141 | params := httprouter.ParamsFromContext(r.Context()) 142 | 143 | fmt.Fprintf(w, "hello, %s!\n", params.ByName("name")) 144 | } 145 | ``` 146 | 147 | Alternatively, one can also use `params := r.Context().Value(httprouter.ParamsKey)` instead of the helper function. 148 | 149 | Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. 150 | 151 | ## Automatic OPTIONS responses and CORS 152 | 153 | One might wish to modify automatic responses to OPTIONS requests, e.g. to support [CORS preflight requests](https://developer.mozilla.org/en-US/docs/Glossary/preflight_request) or to set other headers. 154 | This can be achieved using the [`Router.GlobalOPTIONS`](https://godoc.org/github.com/julienschmidt/httprouter#Router.GlobalOPTIONS) handler: 155 | 156 | ```go 157 | router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 | if r.Header.Get("Access-Control-Request-Method") != "" { 159 | // Set CORS headers 160 | header := w.Header() 161 | header.Set("Access-Control-Allow-Methods", header.Get("Allow")) 162 | header.Set("Access-Control-Allow-Origin", "*") 163 | } 164 | 165 | // Adjust status code to 204 166 | w.WriteHeader(http.StatusNoContent) 167 | }) 168 | ``` 169 | 170 | ## Where can I find Middleware *X*? 171 | 172 | This package just provides a very efficient request router with a few extra features. The router is just a [`http.Handler`](https://golang.org/pkg/net/http/#Handler), you can chain any http.Handler compatible middleware before the router, for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers). Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy! 173 | 174 | Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter). 175 | 176 | ### Multi-domain / Sub-domains 177 | 178 | Here is a quick example: Does your server serve multiple domains / hosts? 179 | You want to use sub-domains? 180 | Define a router per host! 181 | 182 | ```go 183 | // We need an object that implements the http.Handler interface. 184 | // Therefore we need a type for which we implement the ServeHTTP method. 185 | // We just use a map here, in which we map host names (with port) to http.Handlers 186 | type HostSwitch map[string]http.Handler 187 | 188 | // Implement the ServeHTTP method on our new type 189 | func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { 190 | // Check if a http.Handler is registered for the given host. 191 | // If yes, use it to handle the request. 192 | if handler := hs[r.Host]; handler != nil { 193 | handler.ServeHTTP(w, r) 194 | } else { 195 | // Handle host names for which no handler is registered 196 | http.Error(w, "Forbidden", 403) // Or Redirect? 197 | } 198 | } 199 | 200 | func main() { 201 | // Initialize a router as usual 202 | router := httprouter.New() 203 | router.GET("/", Index) 204 | router.GET("/hello/:name", Hello) 205 | 206 | // Make a new HostSwitch and insert the router (our http handler) 207 | // for example.com and port 12345 208 | hs := make(HostSwitch) 209 | hs["example.com:12345"] = router 210 | 211 | // Use the HostSwitch to listen and serve on port 12345 212 | log.Fatal(http.ListenAndServe(":12345", hs)) 213 | } 214 | ``` 215 | 216 | ### Basic Authentication 217 | 218 | Another quick example: Basic Authentication (RFC 2617) for handles: 219 | 220 | ```go 221 | package main 222 | 223 | import ( 224 | "fmt" 225 | "log" 226 | "net/http" 227 | 228 | "github.com/julienschmidt/httprouter" 229 | ) 230 | 231 | func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle { 232 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 233 | // Get the Basic Authentication credentials 234 | user, password, hasAuth := r.BasicAuth() 235 | 236 | if hasAuth && user == requiredUser && password == requiredPassword { 237 | // Delegate request to the given handle 238 | h(w, r, ps) 239 | } else { 240 | // Request Basic Authentication otherwise 241 | w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") 242 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 243 | } 244 | } 245 | } 246 | 247 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 248 | fmt.Fprint(w, "Not protected!\n") 249 | } 250 | 251 | func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 252 | fmt.Fprint(w, "Protected!\n") 253 | } 254 | 255 | func main() { 256 | user := "gordon" 257 | pass := "secret!" 258 | 259 | router := httprouter.New() 260 | router.GET("/", Index) 261 | router.GET("/protected/", BasicAuth(Protected, user, pass)) 262 | 263 | log.Fatal(http.ListenAndServe(":8080", router)) 264 | } 265 | ``` 266 | 267 | ## Chaining with the NotFound handler 268 | 269 | **NOTE: It might be required to set [`Router.HandleMethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** 270 | 271 | You can use another [`http.Handler`](https://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [`Router.NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining. 272 | 273 | ### Static files 274 | 275 | The `NotFound` handler can for example be used to serve static files from the root path `/` (like an `index.html` file along with other assets): 276 | 277 | ```go 278 | // Serve static files from the ./public directory 279 | router.NotFound = http.FileServer(http.Dir("public")) 280 | ``` 281 | 282 | But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`. 283 | 284 | ## Web Frameworks based on HttpRouter 285 | 286 | If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: 287 | 288 | * [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework 289 | * [api2go](https://github.com/manyminds/api2go): A JSON API Implementation for Go 290 | * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance 291 | * [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go 292 | * [goMiddlewareChain](https://github.com/TobiEiss/goMiddlewareChain): An express.js-like-middleware-chain 293 | * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine 294 | * [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow 295 | * [httpway](https://github.com/corneldamian/httpway): Simple middleware extension with context for httprouter and a server with gracefully shutdown support 296 | * [intake](https://github.com/dbubel/intake): intake is a minimal http framework with enphasis on middleware groups 297 | * [Jett](https://github.com/saurabh0719/jett): A lightweight framework with subrouters, graceful shutdown and middleware at all levels. 298 | * [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context 299 | * [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba 300 | * [nchi](https://github.com/muir/nchi): provides a [chi](https://github.com/go-chi/chi)-like framework using [nject](https://github.com/muir/nject) for flexibility and ease-of-use 301 | * [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang 302 | * [pbgo](https://github.com/chai2010/pbgo): pbgo is a mini RPC/REST framework based on Protobuf 303 | * [River](https://github.com/abiosoft/river): River is a simple and lightweight REST server 304 | * [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts 305 | * [xmux](https://github.com/rs/xmux): xmux is a httprouter fork on top of xhandler (net/context aware) 306 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/julienschmidt/httprouter 2 | 3 | go 1.7 4 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 9 | // for p, eliminating . and .. elements. 10 | // 11 | // The following rules are applied iteratively until no further processing can 12 | // be done: 13 | // 1. Replace multiple slashes with a single slash. 14 | // 2. Eliminate each . path name element (the current directory). 15 | // 3. Eliminate each inner .. path name element (the parent directory) 16 | // along with the non-.. element that precedes it. 17 | // 4. Eliminate .. elements that begin a rooted path: 18 | // that is, replace "/.." by "/" at the beginning of a path. 19 | // 20 | // If the result of this process is an empty string, "/" is returned 21 | func CleanPath(p string) string { 22 | const stackBufSize = 128 23 | 24 | // Turn empty string into "/" 25 | if p == "" { 26 | return "/" 27 | } 28 | 29 | // Reasonably sized buffer on stack to avoid allocations in the common case. 30 | // If a larger buffer is required, it gets allocated dynamically. 31 | buf := make([]byte, 0, stackBufSize) 32 | 33 | n := len(p) 34 | 35 | // Invariants: 36 | // reading from path; r is index of next byte to process. 37 | // writing to buf; w is index of next byte to write. 38 | 39 | // path must start with '/' 40 | r := 1 41 | w := 1 42 | 43 | if p[0] != '/' { 44 | r = 0 45 | 46 | if n+1 > stackBufSize { 47 | buf = make([]byte, n+1) 48 | } else { 49 | buf = buf[:n+1] 50 | } 51 | buf[0] = '/' 52 | } 53 | 54 | trailing := n > 1 && p[n-1] == '/' 55 | 56 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 57 | // gets completely inlined (bufApp calls). 58 | // So in contrast to the path package this loop has no expensive function 59 | // calls (except make, if needed). 60 | 61 | for r < n { 62 | switch { 63 | case p[r] == '/': 64 | // empty path element, trailing slash is added after the end 65 | r++ 66 | 67 | case p[r] == '.' && r+1 == n: 68 | trailing = true 69 | r++ 70 | 71 | case p[r] == '.' && p[r+1] == '/': 72 | // . element 73 | r += 2 74 | 75 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 76 | // .. element: remove to last / 77 | r += 3 78 | 79 | if w > 1 { 80 | // can backtrack 81 | w-- 82 | 83 | if len(buf) == 0 { 84 | for w > 1 && p[w] != '/' { 85 | w-- 86 | } 87 | } else { 88 | for w > 1 && buf[w] != '/' { 89 | w-- 90 | } 91 | } 92 | } 93 | 94 | default: 95 | // Real path element. 96 | // Add slash if needed 97 | if w > 1 { 98 | bufApp(&buf, p, w, '/') 99 | w++ 100 | } 101 | 102 | // Copy element 103 | for r < n && p[r] != '/' { 104 | bufApp(&buf, p, w, p[r]) 105 | w++ 106 | r++ 107 | } 108 | } 109 | } 110 | 111 | // Re-append trailing slash 112 | if trailing && w > 1 { 113 | bufApp(&buf, p, w, '/') 114 | w++ 115 | } 116 | 117 | // If the original string was not modified (or only shortened at the end), 118 | // return the respective substring of the original string. 119 | // Otherwise return a new string from the buffer. 120 | if len(buf) == 0 { 121 | return p[:w] 122 | } 123 | return string(buf[:w]) 124 | } 125 | 126 | // Internal helper to lazily create a buffer if necessary. 127 | // Calls to this function get inlined. 128 | func bufApp(buf *[]byte, s string, w int, c byte) { 129 | b := *buf 130 | if len(b) == 0 { 131 | // No modification of the original string so far. 132 | // If the next character is the same as in the original string, we do 133 | // not yet have to allocate a buffer. 134 | if s[w] == c { 135 | return 136 | } 137 | 138 | // Otherwise use either the stack buffer, if it is large enough, or 139 | // allocate a new buffer on the heap, and copy all previous characters. 140 | if l := len(s); l > cap(b) { 141 | *buf = make([]byte, len(s)) 142 | } else { 143 | *buf = (*buf)[:l] 144 | } 145 | b = *buf 146 | 147 | copy(b, s[:w]) 148 | } 149 | b[w] = c 150 | } 151 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | import ( 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type cleanPathTest struct { 14 | path, result string 15 | } 16 | 17 | var cleanTests = []cleanPathTest{ 18 | // Already clean 19 | {"/", "/"}, 20 | {"/abc", "/abc"}, 21 | {"/a/b/c", "/a/b/c"}, 22 | {"/abc/", "/abc/"}, 23 | {"/a/b/c/", "/a/b/c/"}, 24 | 25 | // missing root 26 | {"", "/"}, 27 | {"a/", "/a/"}, 28 | {"abc", "/abc"}, 29 | {"abc/def", "/abc/def"}, 30 | {"a/b/c", "/a/b/c"}, 31 | 32 | // Remove doubled slash 33 | {"//", "/"}, 34 | {"/abc//", "/abc/"}, 35 | {"/abc/def//", "/abc/def/"}, 36 | {"/a/b/c//", "/a/b/c/"}, 37 | {"/abc//def//ghi", "/abc/def/ghi"}, 38 | {"//abc", "/abc"}, 39 | {"///abc", "/abc"}, 40 | {"//abc//", "/abc/"}, 41 | 42 | // Remove . elements 43 | {".", "/"}, 44 | {"./", "/"}, 45 | {"/abc/./def", "/abc/def"}, 46 | {"/./abc/def", "/abc/def"}, 47 | {"/abc/.", "/abc/"}, 48 | 49 | // Remove .. elements 50 | {"..", "/"}, 51 | {"../", "/"}, 52 | {"../../", "/"}, 53 | {"../..", "/"}, 54 | {"../../abc", "/abc"}, 55 | {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, 56 | {"/abc/def/../ghi/../jkl", "/abc/jkl"}, 57 | {"/abc/def/..", "/abc"}, 58 | {"/abc/def/../..", "/"}, 59 | {"/abc/def/../../..", "/"}, 60 | {"/abc/def/../../..", "/"}, 61 | {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, 62 | 63 | // Combinations 64 | {"abc/./../def", "/def"}, 65 | {"abc//./../def", "/def"}, 66 | {"abc/../../././../def", "/def"}, 67 | } 68 | 69 | func TestPathClean(t *testing.T) { 70 | for _, test := range cleanTests { 71 | if s := CleanPath(test.path); s != test.result { 72 | t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) 73 | } 74 | if s := CleanPath(test.result); s != test.result { 75 | t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) 76 | } 77 | } 78 | } 79 | 80 | func TestPathCleanMallocs(t *testing.T) { 81 | if testing.Short() { 82 | t.Skip("skipping malloc count in short mode") 83 | } 84 | 85 | for _, test := range cleanTests { 86 | test := test 87 | allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) 88 | if allocs > 0 { 89 | t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) 90 | } 91 | } 92 | } 93 | 94 | func BenchmarkPathClean(b *testing.B) { 95 | b.ReportAllocs() 96 | 97 | for i := 0; i < b.N; i++ { 98 | for _, test := range cleanTests { 99 | CleanPath(test.path) 100 | } 101 | } 102 | } 103 | 104 | func genLongPaths() (testPaths []cleanPathTest) { 105 | for i := 1; i <= 1234; i++ { 106 | ss := strings.Repeat("a", i) 107 | 108 | correctPath := "/" + ss 109 | testPaths = append(testPaths, cleanPathTest{ 110 | path: correctPath, 111 | result: correctPath, 112 | }, cleanPathTest{ 113 | path: ss, 114 | result: correctPath, 115 | }, cleanPathTest{ 116 | path: "//" + ss, 117 | result: correctPath, 118 | }, cleanPathTest{ 119 | path: "/" + ss + "/b/..", 120 | result: correctPath, 121 | }) 122 | } 123 | return testPaths 124 | } 125 | 126 | func TestPathCleanLong(t *testing.T) { 127 | cleanTests := genLongPaths() 128 | 129 | for _, test := range cleanTests { 130 | if s := CleanPath(test.path); s != test.result { 131 | t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) 132 | } 133 | if s := CleanPath(test.result); s != test.result { 134 | t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) 135 | } 136 | } 137 | } 138 | 139 | func BenchmarkPathCleanLong(b *testing.B) { 140 | cleanTests := genLongPaths() 141 | b.ResetTimer() 142 | b.ReportAllocs() 143 | 144 | for i := 0; i < b.N; i++ { 145 | for _, test := range cleanTests { 146 | CleanPath(test.path) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | // Package httprouter is a trie based high performance HTTP request router. 6 | // 7 | // A trivial example is: 8 | // 9 | // package main 10 | // 11 | // import ( 12 | // "fmt" 13 | // "github.com/julienschmidt/httprouter" 14 | // "net/http" 15 | // "log" 16 | // ) 17 | // 18 | // func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 19 | // fmt.Fprint(w, "Welcome!\n") 20 | // } 21 | // 22 | // func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 23 | // fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 24 | // } 25 | // 26 | // func main() { 27 | // router := httprouter.New() 28 | // router.GET("/", Index) 29 | // router.GET("/hello/:name", Hello) 30 | // 31 | // log.Fatal(http.ListenAndServe(":8080", router)) 32 | // } 33 | // 34 | // The router matches incoming requests by the request method and the path. 35 | // If a handle is registered for this path and method, the router delegates the 36 | // request to that function. 37 | // For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS shortcut functions exist to 38 | // register handles, for all other methods router.Handle can be used. 39 | // 40 | // The registered path, against which the router matches incoming requests, can 41 | // contain two types of parameters: 42 | // Syntax Type 43 | // :name named parameter 44 | // *name catch-all parameter 45 | // 46 | // Named parameters are dynamic path segments. They match anything until the 47 | // next '/' or the path end: 48 | // Path: /blog/:category/:post 49 | // 50 | // Requests: 51 | // /blog/go/request-routers match: category="go", post="request-routers" 52 | // /blog/go/request-routers/ no match, but the router would redirect 53 | // /blog/go/ no match 54 | // /blog/go/request-routers/comments no match 55 | // 56 | // Catch-all parameters match anything until the path end, including the 57 | // directory index (the '/' before the catch-all). Since they match anything 58 | // until the end, catch-all parameters must always be the final path element. 59 | // Path: /files/*filepath 60 | // 61 | // Requests: 62 | // /files/ match: filepath="/" 63 | // /files/LICENSE match: filepath="/LICENSE" 64 | // /files/templates/article.html match: filepath="/templates/article.html" 65 | // /files no match, but the router would redirect 66 | // 67 | // The value of parameters is saved as a slice of the Param struct, consisting 68 | // each of a key and a value. The slice is passed to the Handle func as a third 69 | // parameter. 70 | // There are two ways to retrieve the value of a parameter: 71 | // // by the name of the parameter 72 | // user := ps.ByName("user") // defined by :user or *user 73 | // 74 | // // by the index of the parameter. This way you can also get the name (key) 75 | // thirdKey := ps[2].Key // the name of the 3rd parameter 76 | // thirdValue := ps[2].Value // the value of the 3rd parameter 77 | package httprouter 78 | 79 | import ( 80 | "context" 81 | "net/http" 82 | "strings" 83 | "sync" 84 | ) 85 | 86 | // Handle is a function that can be registered to a route to handle HTTP 87 | // requests. Like http.HandlerFunc, but has a third parameter for the values of 88 | // wildcards (path variables). 89 | type Handle func(http.ResponseWriter, *http.Request, Params) 90 | 91 | // Param is a single URL parameter, consisting of a key and a value. 92 | type Param struct { 93 | Key string 94 | Value string 95 | } 96 | 97 | // Params is a Param-slice, as returned by the router. 98 | // The slice is ordered, the first URL parameter is also the first slice value. 99 | // It is therefore safe to read values by the index. 100 | type Params []Param 101 | 102 | // ByName returns the value of the first Param which key matches the given name. 103 | // If no matching Param is found, an empty string is returned. 104 | func (ps Params) ByName(name string) string { 105 | for _, p := range ps { 106 | if p.Key == name { 107 | return p.Value 108 | } 109 | } 110 | return "" 111 | } 112 | 113 | type paramsKey struct{} 114 | 115 | // ParamsKey is the request context key under which URL params are stored. 116 | var ParamsKey = paramsKey{} 117 | 118 | // ParamsFromContext pulls the URL parameters from a request context, 119 | // or returns nil if none are present. 120 | func ParamsFromContext(ctx context.Context) Params { 121 | p, _ := ctx.Value(ParamsKey).(Params) 122 | return p 123 | } 124 | 125 | // MatchedRoutePathParam is the Param name under which the path of the matched 126 | // route is stored, if Router.SaveMatchedRoutePath is set. 127 | var MatchedRoutePathParam = "$matchedRoutePath" 128 | 129 | // MatchedRoutePath retrieves the path of the matched route. 130 | // Router.SaveMatchedRoutePath must have been enabled when the respective 131 | // handler was added, otherwise this function always returns an empty string. 132 | func (ps Params) MatchedRoutePath() string { 133 | return ps.ByName(MatchedRoutePathParam) 134 | } 135 | 136 | // Router is a http.Handler which can be used to dispatch requests to different 137 | // handler functions via configurable routes 138 | type Router struct { 139 | trees map[string]*node 140 | 141 | paramsPool sync.Pool 142 | maxParams uint16 143 | 144 | // If enabled, adds the matched route path onto the http.Request context 145 | // before invoking the handler. 146 | // The matched route path is only added to handlers of routes that were 147 | // registered when this option was enabled. 148 | SaveMatchedRoutePath bool 149 | 150 | // Enables automatic redirection if the current route can't be matched but a 151 | // handler for the path with (without) the trailing slash exists. 152 | // For example if /foo/ is requested but a route only exists for /foo, the 153 | // client is redirected to /foo with http status code 301 for GET requests 154 | // and 308 for all other request methods. 155 | RedirectTrailingSlash bool 156 | 157 | // If enabled, the router tries to fix the current request path, if no 158 | // handle is registered for it. 159 | // First superfluous path elements like ../ or // are removed. 160 | // Afterwards the router does a case-insensitive lookup of the cleaned path. 161 | // If a handle can be found for this route, the router makes a redirection 162 | // to the corrected path with status code 301 for GET requests and 308 for 163 | // all other request methods. 164 | // For example /FOO and /..//Foo could be redirected to /foo. 165 | // RedirectTrailingSlash is independent of this option. 166 | RedirectFixedPath bool 167 | 168 | // If enabled, the router checks if another method is allowed for the 169 | // current route, if the current request can not be routed. 170 | // If this is the case, the request is answered with 'Method Not Allowed' 171 | // and HTTP status code 405. 172 | // If no other Method is allowed, the request is delegated to the NotFound 173 | // handler. 174 | HandleMethodNotAllowed bool 175 | 176 | // If enabled, the router automatically replies to OPTIONS requests. 177 | // Custom OPTIONS handlers take priority over automatic replies. 178 | HandleOPTIONS bool 179 | 180 | // An optional http.Handler that is called on automatic OPTIONS requests. 181 | // The handler is only called if HandleOPTIONS is true and no OPTIONS 182 | // handler for the specific path was set. 183 | // The "Allowed" header is set before calling the handler. 184 | GlobalOPTIONS http.Handler 185 | 186 | // Cached value of global (*) allowed methods 187 | globalAllowed string 188 | 189 | // Configurable http.Handler which is called when no matching route is 190 | // found. If it is not set, http.NotFound is used. 191 | NotFound http.Handler 192 | 193 | // Configurable http.Handler which is called when a request 194 | // cannot be routed and HandleMethodNotAllowed is true. 195 | // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 196 | // The "Allow" header with allowed request methods is set before the handler 197 | // is called. 198 | MethodNotAllowed http.Handler 199 | 200 | // Function to handle panics recovered from http handlers. 201 | // It should be used to generate a error page and return the http error code 202 | // 500 (Internal Server Error). 203 | // The handler can be used to keep your server from crashing because of 204 | // unrecovered panics. 205 | PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 206 | } 207 | 208 | // Make sure the Router conforms with the http.Handler interface 209 | var _ http.Handler = New() 210 | 211 | // New returns a new initialized Router. 212 | // Path auto-correction, including trailing slashes, is enabled by default. 213 | func New() *Router { 214 | return &Router{ 215 | RedirectTrailingSlash: true, 216 | RedirectFixedPath: true, 217 | HandleMethodNotAllowed: true, 218 | HandleOPTIONS: true, 219 | } 220 | } 221 | 222 | func (r *Router) getParams() *Params { 223 | ps, _ := r.paramsPool.Get().(*Params) 224 | *ps = (*ps)[0:0] // reset slice 225 | return ps 226 | } 227 | 228 | func (r *Router) putParams(ps *Params) { 229 | if ps != nil { 230 | r.paramsPool.Put(ps) 231 | } 232 | } 233 | 234 | func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { 235 | return func(w http.ResponseWriter, req *http.Request, ps Params) { 236 | if ps == nil { 237 | psp := r.getParams() 238 | ps = (*psp)[0:1] 239 | ps[0] = Param{Key: MatchedRoutePathParam, Value: path} 240 | handle(w, req, ps) 241 | r.putParams(psp) 242 | } else { 243 | ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) 244 | handle(w, req, ps) 245 | } 246 | } 247 | } 248 | 249 | // GET is a shortcut for router.Handle(http.MethodGet, path, handle) 250 | func (r *Router) GET(path string, handle Handle) { 251 | r.Handle(http.MethodGet, path, handle) 252 | } 253 | 254 | // HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) 255 | func (r *Router) HEAD(path string, handle Handle) { 256 | r.Handle(http.MethodHead, path, handle) 257 | } 258 | 259 | // OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) 260 | func (r *Router) OPTIONS(path string, handle Handle) { 261 | r.Handle(http.MethodOptions, path, handle) 262 | } 263 | 264 | // POST is a shortcut for router.Handle(http.MethodPost, path, handle) 265 | func (r *Router) POST(path string, handle Handle) { 266 | r.Handle(http.MethodPost, path, handle) 267 | } 268 | 269 | // PUT is a shortcut for router.Handle(http.MethodPut, path, handle) 270 | func (r *Router) PUT(path string, handle Handle) { 271 | r.Handle(http.MethodPut, path, handle) 272 | } 273 | 274 | // PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) 275 | func (r *Router) PATCH(path string, handle Handle) { 276 | r.Handle(http.MethodPatch, path, handle) 277 | } 278 | 279 | // DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) 280 | func (r *Router) DELETE(path string, handle Handle) { 281 | r.Handle(http.MethodDelete, path, handle) 282 | } 283 | 284 | // Handle registers a new request handle with the given path and method. 285 | // 286 | // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 287 | // functions can be used. 288 | // 289 | // This function is intended for bulk loading and to allow the usage of less 290 | // frequently used, non-standardized or custom methods (e.g. for internal 291 | // communication with a proxy). 292 | func (r *Router) Handle(method, path string, handle Handle) { 293 | varsCount := uint16(0) 294 | 295 | if method == "" { 296 | panic("method must not be empty") 297 | } 298 | if len(path) < 1 || path[0] != '/' { 299 | panic("path must begin with '/' in path '" + path + "'") 300 | } 301 | if handle == nil { 302 | panic("handle must not be nil") 303 | } 304 | 305 | if r.SaveMatchedRoutePath { 306 | varsCount++ 307 | handle = r.saveMatchedRoutePath(path, handle) 308 | } 309 | 310 | if r.trees == nil { 311 | r.trees = make(map[string]*node) 312 | } 313 | 314 | root := r.trees[method] 315 | if root == nil { 316 | root = new(node) 317 | r.trees[method] = root 318 | 319 | r.globalAllowed = r.allowed("*", "") 320 | } 321 | 322 | root.addRoute(path, handle) 323 | 324 | // Update maxParams 325 | if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { 326 | r.maxParams = paramsCount + varsCount 327 | } 328 | 329 | // Lazy-init paramsPool alloc func 330 | if r.paramsPool.New == nil && r.maxParams > 0 { 331 | r.paramsPool.New = func() interface{} { 332 | ps := make(Params, 0, r.maxParams) 333 | return &ps 334 | } 335 | } 336 | } 337 | 338 | // Handler is an adapter which allows the usage of an http.Handler as a 339 | // request handle. 340 | // The Params are available in the request context under ParamsKey. 341 | func (r *Router) Handler(method, path string, handler http.Handler) { 342 | r.Handle(method, path, 343 | func(w http.ResponseWriter, req *http.Request, p Params) { 344 | if len(p) > 0 { 345 | ctx := req.Context() 346 | ctx = context.WithValue(ctx, ParamsKey, p) 347 | req = req.WithContext(ctx) 348 | } 349 | handler.ServeHTTP(w, req) 350 | }, 351 | ) 352 | } 353 | 354 | // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a 355 | // request handle. 356 | func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { 357 | r.Handler(method, path, handler) 358 | } 359 | 360 | // ServeFiles serves files from the given file system root. 361 | // The path must end with "/*filepath", files are then served from the local 362 | // path /defined/root/dir/*filepath. 363 | // For example if root is "/etc" and *filepath is "passwd", the local file 364 | // "/etc/passwd" would be served. 365 | // Internally a http.FileServer is used, therefore http.NotFound is used instead 366 | // of the Router's NotFound handler. 367 | // To use the operating system's file system implementation, 368 | // use http.Dir: 369 | // router.ServeFiles("/src/*filepath", http.Dir("/var/www")) 370 | func (r *Router) ServeFiles(path string, root http.FileSystem) { 371 | if len(path) < 10 || path[len(path)-10:] != "/*filepath" { 372 | panic("path must end with /*filepath in path '" + path + "'") 373 | } 374 | 375 | fileServer := http.FileServer(root) 376 | 377 | r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { 378 | req.URL.Path = ps.ByName("filepath") 379 | fileServer.ServeHTTP(w, req) 380 | }) 381 | } 382 | 383 | func (r *Router) recv(w http.ResponseWriter, req *http.Request) { 384 | if rcv := recover(); rcv != nil { 385 | r.PanicHandler(w, req, rcv) 386 | } 387 | } 388 | 389 | // Lookup allows the manual lookup of a method + path combo. 390 | // This is e.g. useful to build a framework around this router. 391 | // If the path was found, it returns the handle function and the path parameter 392 | // values. Otherwise the third return value indicates whether a redirection to 393 | // the same path with an extra / without the trailing slash should be performed. 394 | func (r *Router) Lookup(method, path string) (Handle, Params, bool) { 395 | if root := r.trees[method]; root != nil { 396 | handle, ps, tsr := root.getValue(path, r.getParams) 397 | if handle == nil { 398 | r.putParams(ps) 399 | return nil, nil, tsr 400 | } 401 | if ps == nil { 402 | return handle, nil, tsr 403 | } 404 | return handle, *ps, tsr 405 | } 406 | return nil, nil, false 407 | } 408 | 409 | func (r *Router) allowed(path, reqMethod string) (allow string) { 410 | allowed := make([]string, 0, 9) 411 | 412 | if path == "*" { // server-wide 413 | // empty method is used for internal calls to refresh the cache 414 | if reqMethod == "" { 415 | for method := range r.trees { 416 | if method == http.MethodOptions { 417 | continue 418 | } 419 | // Add request method to list of allowed methods 420 | allowed = append(allowed, method) 421 | } 422 | } else { 423 | return r.globalAllowed 424 | } 425 | } else { // specific path 426 | for method := range r.trees { 427 | // Skip the requested method - we already tried this one 428 | if method == reqMethod || method == http.MethodOptions { 429 | continue 430 | } 431 | 432 | handle, _, _ := r.trees[method].getValue(path, nil) 433 | if handle != nil { 434 | // Add request method to list of allowed methods 435 | allowed = append(allowed, method) 436 | } 437 | } 438 | } 439 | 440 | if len(allowed) > 0 { 441 | // Add request method to list of allowed methods 442 | if r.HandleOPTIONS { 443 | allowed = append(allowed, http.MethodOptions) 444 | } 445 | 446 | // Sort allowed methods. 447 | // sort.Strings(allowed) unfortunately causes unnecessary allocations 448 | // due to allowed being moved to the heap and interface conversion 449 | for i, l := 1, len(allowed); i < l; i++ { 450 | for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { 451 | allowed[j], allowed[j-1] = allowed[j-1], allowed[j] 452 | } 453 | } 454 | 455 | // return as comma separated list 456 | return strings.Join(allowed, ", ") 457 | } 458 | 459 | return allow 460 | } 461 | 462 | // ServeHTTP makes the router implement the http.Handler interface. 463 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 464 | if r.PanicHandler != nil { 465 | defer r.recv(w, req) 466 | } 467 | 468 | path := req.URL.Path 469 | 470 | if root := r.trees[req.Method]; root != nil { 471 | if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { 472 | if ps != nil { 473 | handle(w, req, *ps) 474 | r.putParams(ps) 475 | } else { 476 | handle(w, req, nil) 477 | } 478 | return 479 | } else if req.Method != http.MethodConnect && path != "/" { 480 | // Moved Permanently, request with GET method 481 | code := http.StatusMovedPermanently 482 | if req.Method != http.MethodGet { 483 | // Permanent Redirect, request with same method 484 | code = http.StatusPermanentRedirect 485 | } 486 | 487 | if tsr && r.RedirectTrailingSlash { 488 | if len(path) > 1 && path[len(path)-1] == '/' { 489 | req.URL.Path = path[:len(path)-1] 490 | } else { 491 | req.URL.Path = path + "/" 492 | } 493 | http.Redirect(w, req, req.URL.String(), code) 494 | return 495 | } 496 | 497 | // Try to fix the request path 498 | if r.RedirectFixedPath { 499 | fixedPath, found := root.findCaseInsensitivePath( 500 | CleanPath(path), 501 | r.RedirectTrailingSlash, 502 | ) 503 | if found { 504 | req.URL.Path = fixedPath 505 | http.Redirect(w, req, req.URL.String(), code) 506 | return 507 | } 508 | } 509 | } 510 | } 511 | 512 | if req.Method == http.MethodOptions && r.HandleOPTIONS { 513 | // Handle OPTIONS requests 514 | if allow := r.allowed(path, http.MethodOptions); allow != "" { 515 | w.Header().Set("Allow", allow) 516 | if r.GlobalOPTIONS != nil { 517 | r.GlobalOPTIONS.ServeHTTP(w, req) 518 | } 519 | return 520 | } 521 | } else if r.HandleMethodNotAllowed { // Handle 405 522 | if allow := r.allowed(path, req.Method); allow != "" { 523 | w.Header().Set("Allow", allow) 524 | if r.MethodNotAllowed != nil { 525 | r.MethodNotAllowed.ServeHTTP(w, req) 526 | } else { 527 | http.Error(w, 528 | http.StatusText(http.StatusMethodNotAllowed), 529 | http.StatusMethodNotAllowed, 530 | ) 531 | } 532 | return 533 | } 534 | } 535 | 536 | // Handle 404 537 | if r.NotFound != nil { 538 | r.NotFound.ServeHTTP(w, req) 539 | } else { 540 | http.NotFound(w, req) 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | type mockResponseWriter struct{} 17 | 18 | func (m *mockResponseWriter) Header() (h http.Header) { 19 | return http.Header{} 20 | } 21 | 22 | func (m *mockResponseWriter) Write(p []byte) (n int, err error) { 23 | return len(p), nil 24 | } 25 | 26 | func (m *mockResponseWriter) WriteString(s string) (n int, err error) { 27 | return len(s), nil 28 | } 29 | 30 | func (m *mockResponseWriter) WriteHeader(int) {} 31 | 32 | func TestParams(t *testing.T) { 33 | ps := Params{ 34 | Param{"param1", "value1"}, 35 | Param{"param2", "value2"}, 36 | Param{"param3", "value3"}, 37 | } 38 | for i := range ps { 39 | if val := ps.ByName(ps[i].Key); val != ps[i].Value { 40 | t.Errorf("Wrong value for %s: Got %s; Want %s", ps[i].Key, val, ps[i].Value) 41 | } 42 | } 43 | if val := ps.ByName("noKey"); val != "" { 44 | t.Errorf("Expected empty string for not found key; got: %s", val) 45 | } 46 | } 47 | 48 | func TestRouter(t *testing.T) { 49 | router := New() 50 | 51 | routed := false 52 | router.Handle(http.MethodGet, "/user/:name", func(w http.ResponseWriter, r *http.Request, ps Params) { 53 | routed = true 54 | want := Params{Param{"name", "gopher"}} 55 | if !reflect.DeepEqual(ps, want) { 56 | t.Fatalf("wrong wildcard values: want %v, got %v", want, ps) 57 | } 58 | }) 59 | 60 | w := new(mockResponseWriter) 61 | 62 | req, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) 63 | router.ServeHTTP(w, req) 64 | 65 | if !routed { 66 | t.Fatal("routing failed") 67 | } 68 | } 69 | 70 | type handlerStruct struct { 71 | handled *bool 72 | } 73 | 74 | func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { 75 | *h.handled = true 76 | } 77 | 78 | func TestRouterAPI(t *testing.T) { 79 | var get, head, options, post, put, patch, delete, handler, handlerFunc bool 80 | 81 | httpHandler := handlerStruct{&handler} 82 | 83 | router := New() 84 | router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 85 | get = true 86 | }) 87 | router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 88 | head = true 89 | }) 90 | router.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 91 | options = true 92 | }) 93 | router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { 94 | post = true 95 | }) 96 | router.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) { 97 | put = true 98 | }) 99 | router.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) { 100 | patch = true 101 | }) 102 | router.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) { 103 | delete = true 104 | }) 105 | router.Handler(http.MethodGet, "/Handler", httpHandler) 106 | router.HandlerFunc(http.MethodGet, "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) { 107 | handlerFunc = true 108 | }) 109 | 110 | w := new(mockResponseWriter) 111 | 112 | r, _ := http.NewRequest(http.MethodGet, "/GET", nil) 113 | router.ServeHTTP(w, r) 114 | if !get { 115 | t.Error("routing GET failed") 116 | } 117 | 118 | r, _ = http.NewRequest(http.MethodHead, "/GET", nil) 119 | router.ServeHTTP(w, r) 120 | if !head { 121 | t.Error("routing HEAD failed") 122 | } 123 | 124 | r, _ = http.NewRequest(http.MethodOptions, "/GET", nil) 125 | router.ServeHTTP(w, r) 126 | if !options { 127 | t.Error("routing OPTIONS failed") 128 | } 129 | 130 | r, _ = http.NewRequest(http.MethodPost, "/POST", nil) 131 | router.ServeHTTP(w, r) 132 | if !post { 133 | t.Error("routing POST failed") 134 | } 135 | 136 | r, _ = http.NewRequest(http.MethodPut, "/PUT", nil) 137 | router.ServeHTTP(w, r) 138 | if !put { 139 | t.Error("routing PUT failed") 140 | } 141 | 142 | r, _ = http.NewRequest(http.MethodPatch, "/PATCH", nil) 143 | router.ServeHTTP(w, r) 144 | if !patch { 145 | t.Error("routing PATCH failed") 146 | } 147 | 148 | r, _ = http.NewRequest(http.MethodDelete, "/DELETE", nil) 149 | router.ServeHTTP(w, r) 150 | if !delete { 151 | t.Error("routing DELETE failed") 152 | } 153 | 154 | r, _ = http.NewRequest(http.MethodGet, "/Handler", nil) 155 | router.ServeHTTP(w, r) 156 | if !handler { 157 | t.Error("routing Handler failed") 158 | } 159 | 160 | r, _ = http.NewRequest(http.MethodGet, "/HandlerFunc", nil) 161 | router.ServeHTTP(w, r) 162 | if !handlerFunc { 163 | t.Error("routing HandlerFunc failed") 164 | } 165 | } 166 | 167 | func TestRouterInvalidInput(t *testing.T) { 168 | router := New() 169 | 170 | handle := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 171 | 172 | recv := catchPanic(func() { 173 | router.Handle("", "/", handle) 174 | }) 175 | if recv == nil { 176 | t.Fatal("registering empty method did not panic") 177 | } 178 | 179 | recv = catchPanic(func() { 180 | router.GET("", handle) 181 | }) 182 | if recv == nil { 183 | t.Fatal("registering empty path did not panic") 184 | } 185 | 186 | recv = catchPanic(func() { 187 | router.GET("noSlashRoot", handle) 188 | }) 189 | if recv == nil { 190 | t.Fatal("registering path not beginning with '/' did not panic") 191 | } 192 | 193 | recv = catchPanic(func() { 194 | router.GET("/", nil) 195 | }) 196 | if recv == nil { 197 | t.Fatal("registering nil handler did not panic") 198 | } 199 | } 200 | 201 | func TestRouterChaining(t *testing.T) { 202 | router1 := New() 203 | router2 := New() 204 | router1.NotFound = router2 205 | 206 | fooHit := false 207 | router1.POST("/foo", func(w http.ResponseWriter, req *http.Request, _ Params) { 208 | fooHit = true 209 | w.WriteHeader(http.StatusOK) 210 | }) 211 | 212 | barHit := false 213 | router2.POST("/bar", func(w http.ResponseWriter, req *http.Request, _ Params) { 214 | barHit = true 215 | w.WriteHeader(http.StatusOK) 216 | }) 217 | 218 | r, _ := http.NewRequest(http.MethodPost, "/foo", nil) 219 | w := httptest.NewRecorder() 220 | router1.ServeHTTP(w, r) 221 | if !(w.Code == http.StatusOK && fooHit) { 222 | t.Errorf("Regular routing failed with router chaining.") 223 | t.FailNow() 224 | } 225 | 226 | r, _ = http.NewRequest(http.MethodPost, "/bar", nil) 227 | w = httptest.NewRecorder() 228 | router1.ServeHTTP(w, r) 229 | if !(w.Code == http.StatusOK && barHit) { 230 | t.Errorf("Chained routing failed with router chaining.") 231 | t.FailNow() 232 | } 233 | 234 | r, _ = http.NewRequest(http.MethodPost, "/qax", nil) 235 | w = httptest.NewRecorder() 236 | router1.ServeHTTP(w, r) 237 | if !(w.Code == http.StatusNotFound) { 238 | t.Errorf("NotFound behavior failed with router chaining.") 239 | t.FailNow() 240 | } 241 | } 242 | 243 | func BenchmarkAllowed(b *testing.B) { 244 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 245 | 246 | router := New() 247 | router.POST("/path", handlerFunc) 248 | router.GET("/path", handlerFunc) 249 | 250 | b.Run("Global", func(b *testing.B) { 251 | b.ReportAllocs() 252 | for i := 0; i < b.N; i++ { 253 | _ = router.allowed("*", http.MethodOptions) 254 | } 255 | }) 256 | b.Run("Path", func(b *testing.B) { 257 | b.ReportAllocs() 258 | for i := 0; i < b.N; i++ { 259 | _ = router.allowed("/path", http.MethodOptions) 260 | } 261 | }) 262 | } 263 | 264 | func TestRouterOPTIONS(t *testing.T) { 265 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 266 | 267 | router := New() 268 | router.POST("/path", handlerFunc) 269 | 270 | // test not allowed 271 | // * (server) 272 | r, _ := http.NewRequest(http.MethodOptions, "*", nil) 273 | w := httptest.NewRecorder() 274 | router.ServeHTTP(w, r) 275 | if !(w.Code == http.StatusOK) { 276 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 277 | } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { 278 | t.Error("unexpected Allow header value: " + allow) 279 | } 280 | 281 | // path 282 | r, _ = http.NewRequest(http.MethodOptions, "/path", nil) 283 | w = httptest.NewRecorder() 284 | router.ServeHTTP(w, r) 285 | if !(w.Code == http.StatusOK) { 286 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 287 | } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { 288 | t.Error("unexpected Allow header value: " + allow) 289 | } 290 | 291 | r, _ = http.NewRequest(http.MethodOptions, "/doesnotexist", nil) 292 | w = httptest.NewRecorder() 293 | router.ServeHTTP(w, r) 294 | if !(w.Code == http.StatusNotFound) { 295 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 296 | } 297 | 298 | // add another method 299 | router.GET("/path", handlerFunc) 300 | 301 | // set a global OPTIONS handler 302 | router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 303 | // Adjust status code to 204 304 | w.WriteHeader(http.StatusNoContent) 305 | }) 306 | 307 | // test again 308 | // * (server) 309 | r, _ = http.NewRequest(http.MethodOptions, "*", nil) 310 | w = httptest.NewRecorder() 311 | router.ServeHTTP(w, r) 312 | if !(w.Code == http.StatusNoContent) { 313 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 314 | } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { 315 | t.Error("unexpected Allow header value: " + allow) 316 | } 317 | 318 | // path 319 | r, _ = http.NewRequest(http.MethodOptions, "/path", nil) 320 | w = httptest.NewRecorder() 321 | router.ServeHTTP(w, r) 322 | if !(w.Code == http.StatusNoContent) { 323 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 324 | } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { 325 | t.Error("unexpected Allow header value: " + allow) 326 | } 327 | 328 | // custom handler 329 | var custom bool 330 | router.OPTIONS("/path", func(w http.ResponseWriter, r *http.Request, _ Params) { 331 | custom = true 332 | }) 333 | 334 | // test again 335 | // * (server) 336 | r, _ = http.NewRequest(http.MethodOptions, "*", nil) 337 | w = httptest.NewRecorder() 338 | router.ServeHTTP(w, r) 339 | if !(w.Code == http.StatusNoContent) { 340 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 341 | } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { 342 | t.Error("unexpected Allow header value: " + allow) 343 | } 344 | if custom { 345 | t.Error("custom handler called on *") 346 | } 347 | 348 | // path 349 | r, _ = http.NewRequest(http.MethodOptions, "/path", nil) 350 | w = httptest.NewRecorder() 351 | router.ServeHTTP(w, r) 352 | if !(w.Code == http.StatusOK) { 353 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) 354 | } 355 | if !custom { 356 | t.Error("custom handler not called") 357 | } 358 | } 359 | 360 | func TestRouterNotAllowed(t *testing.T) { 361 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 362 | 363 | router := New() 364 | router.POST("/path", handlerFunc) 365 | 366 | // test not allowed 367 | r, _ := http.NewRequest(http.MethodGet, "/path", nil) 368 | w := httptest.NewRecorder() 369 | router.ServeHTTP(w, r) 370 | if !(w.Code == http.StatusMethodNotAllowed) { 371 | t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) 372 | } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { 373 | t.Error("unexpected Allow header value: " + allow) 374 | } 375 | 376 | // add another method 377 | router.DELETE("/path", handlerFunc) 378 | router.OPTIONS("/path", handlerFunc) // must be ignored 379 | 380 | // test again 381 | r, _ = http.NewRequest(http.MethodGet, "/path", nil) 382 | w = httptest.NewRecorder() 383 | router.ServeHTTP(w, r) 384 | if !(w.Code == http.StatusMethodNotAllowed) { 385 | t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) 386 | } else if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { 387 | t.Error("unexpected Allow header value: " + allow) 388 | } 389 | 390 | // test custom handler 391 | w = httptest.NewRecorder() 392 | responseText := "custom method" 393 | router.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 394 | w.WriteHeader(http.StatusTeapot) 395 | w.Write([]byte(responseText)) 396 | }) 397 | router.ServeHTTP(w, r) 398 | if got := w.Body.String(); !(got == responseText) { 399 | t.Errorf("unexpected response got %q want %q", got, responseText) 400 | } 401 | if w.Code != http.StatusTeapot { 402 | t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot) 403 | } 404 | if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { 405 | t.Error("unexpected Allow header value: " + allow) 406 | } 407 | } 408 | 409 | func TestRouterNotFound(t *testing.T) { 410 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 411 | 412 | router := New() 413 | router.GET("/path", handlerFunc) 414 | router.GET("/dir/", handlerFunc) 415 | router.GET("/", handlerFunc) 416 | 417 | testRoutes := []struct { 418 | route string 419 | code int 420 | location string 421 | }{ 422 | {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/ 423 | {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/ 424 | {"", http.StatusMovedPermanently, "/"}, // TSR +/ 425 | {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case 426 | {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case 427 | {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ 428 | {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ 429 | {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath 430 | {"/nope", http.StatusNotFound, ""}, // NotFound 431 | } 432 | for _, tr := range testRoutes { 433 | r, _ := http.NewRequest(http.MethodGet, tr.route, nil) 434 | w := httptest.NewRecorder() 435 | router.ServeHTTP(w, r) 436 | if !(w.Code == tr.code && (w.Code == http.StatusNotFound || fmt.Sprint(w.Header().Get("Location")) == tr.location)) { 437 | t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header().Get("Location")) 438 | } 439 | } 440 | 441 | // Test custom not found handler 442 | var notFound bool 443 | router.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 444 | rw.WriteHeader(http.StatusNotFound) 445 | notFound = true 446 | }) 447 | r, _ := http.NewRequest(http.MethodGet, "/nope", nil) 448 | w := httptest.NewRecorder() 449 | router.ServeHTTP(w, r) 450 | if !(w.Code == http.StatusNotFound && notFound == true) { 451 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) 452 | } 453 | 454 | // Test other method than GET (want 308 instead of 301) 455 | router.PATCH("/path", handlerFunc) 456 | r, _ = http.NewRequest(http.MethodPatch, "/path/", nil) 457 | w = httptest.NewRecorder() 458 | router.ServeHTTP(w, r) 459 | if !(w.Code == http.StatusPermanentRedirect && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { 460 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) 461 | } 462 | 463 | // Test special case where no node for the prefix "/" exists 464 | router = New() 465 | router.GET("/a", handlerFunc) 466 | r, _ = http.NewRequest(http.MethodGet, "/", nil) 467 | w = httptest.NewRecorder() 468 | router.ServeHTTP(w, r) 469 | if !(w.Code == http.StatusNotFound) { 470 | t.Errorf("NotFound handling route / failed: Code=%d", w.Code) 471 | } 472 | } 473 | 474 | func TestRouterPanicHandler(t *testing.T) { 475 | router := New() 476 | panicHandled := false 477 | 478 | router.PanicHandler = func(rw http.ResponseWriter, r *http.Request, p interface{}) { 479 | panicHandled = true 480 | } 481 | 482 | router.Handle(http.MethodPut, "/user/:name", func(_ http.ResponseWriter, _ *http.Request, _ Params) { 483 | panic("oops!") 484 | }) 485 | 486 | w := new(mockResponseWriter) 487 | req, _ := http.NewRequest(http.MethodPut, "/user/gopher", nil) 488 | 489 | defer func() { 490 | if rcv := recover(); rcv != nil { 491 | t.Fatal("handling panic failed") 492 | } 493 | }() 494 | 495 | router.ServeHTTP(w, req) 496 | 497 | if !panicHandled { 498 | t.Fatal("simulating failed") 499 | } 500 | } 501 | 502 | func TestRouterLookup(t *testing.T) { 503 | routed := false 504 | wantHandle := func(_ http.ResponseWriter, _ *http.Request, _ Params) { 505 | routed = true 506 | } 507 | wantParams := Params{Param{"name", "gopher"}} 508 | 509 | router := New() 510 | 511 | // try empty router first 512 | handle, _, tsr := router.Lookup(http.MethodGet, "/nope") 513 | if handle != nil { 514 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 515 | } 516 | if tsr { 517 | t.Error("Got wrong TSR recommendation!") 518 | } 519 | 520 | // insert route and try again 521 | router.GET("/user/:name", wantHandle) 522 | handle, params, _ := router.Lookup(http.MethodGet, "/user/gopher") 523 | if handle == nil { 524 | t.Fatal("Got no handle!") 525 | } else { 526 | handle(nil, nil, nil) 527 | if !routed { 528 | t.Fatal("Routing failed!") 529 | } 530 | } 531 | if !reflect.DeepEqual(params, wantParams) { 532 | t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) 533 | } 534 | routed = false 535 | 536 | // route without param 537 | router.GET("/user", wantHandle) 538 | handle, params, _ = router.Lookup(http.MethodGet, "/user") 539 | if handle == nil { 540 | t.Fatal("Got no handle!") 541 | } else { 542 | handle(nil, nil, nil) 543 | if !routed { 544 | t.Fatal("Routing failed!") 545 | } 546 | } 547 | if params != nil { 548 | t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) 549 | } 550 | 551 | handle, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") 552 | if handle != nil { 553 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 554 | } 555 | if !tsr { 556 | t.Error("Got no TSR recommendation!") 557 | } 558 | 559 | handle, _, tsr = router.Lookup(http.MethodGet, "/nope") 560 | if handle != nil { 561 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 562 | } 563 | if tsr { 564 | t.Error("Got wrong TSR recommendation!") 565 | } 566 | } 567 | 568 | func TestRouterParamsFromContext(t *testing.T) { 569 | routed := false 570 | 571 | wantParams := Params{Param{"name", "gopher"}} 572 | handlerFunc := func(_ http.ResponseWriter, req *http.Request) { 573 | // get params from request context 574 | params := ParamsFromContext(req.Context()) 575 | 576 | if !reflect.DeepEqual(params, wantParams) { 577 | t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) 578 | } 579 | 580 | routed = true 581 | } 582 | 583 | var nilParams Params 584 | handlerFuncNil := func(_ http.ResponseWriter, req *http.Request) { 585 | // get params from request context 586 | params := ParamsFromContext(req.Context()) 587 | 588 | if !reflect.DeepEqual(params, nilParams) { 589 | t.Fatalf("Wrong parameter values: want %v, got %v", nilParams, params) 590 | } 591 | 592 | routed = true 593 | } 594 | router := New() 595 | router.HandlerFunc(http.MethodGet, "/user", handlerFuncNil) 596 | router.HandlerFunc(http.MethodGet, "/user/:name", handlerFunc) 597 | 598 | w := new(mockResponseWriter) 599 | r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) 600 | router.ServeHTTP(w, r) 601 | if !routed { 602 | t.Fatal("Routing failed!") 603 | } 604 | 605 | routed = false 606 | r, _ = http.NewRequest(http.MethodGet, "/user", nil) 607 | router.ServeHTTP(w, r) 608 | if !routed { 609 | t.Fatal("Routing failed!") 610 | } 611 | } 612 | 613 | func TestRouterMatchedRoutePath(t *testing.T) { 614 | route1 := "/user/:name" 615 | routed1 := false 616 | handle1 := func(_ http.ResponseWriter, req *http.Request, ps Params) { 617 | route := ps.MatchedRoutePath() 618 | if route != route1 { 619 | t.Fatalf("Wrong matched route: want %s, got %s", route1, route) 620 | } 621 | routed1 = true 622 | } 623 | 624 | route2 := "/user/:name/details" 625 | routed2 := false 626 | handle2 := func(_ http.ResponseWriter, req *http.Request, ps Params) { 627 | route := ps.MatchedRoutePath() 628 | if route != route2 { 629 | t.Fatalf("Wrong matched route: want %s, got %s", route2, route) 630 | } 631 | routed2 = true 632 | } 633 | 634 | route3 := "/" 635 | routed3 := false 636 | handle3 := func(_ http.ResponseWriter, req *http.Request, ps Params) { 637 | route := ps.MatchedRoutePath() 638 | if route != route3 { 639 | t.Fatalf("Wrong matched route: want %s, got %s", route3, route) 640 | } 641 | routed3 = true 642 | } 643 | 644 | router := New() 645 | router.SaveMatchedRoutePath = true 646 | router.Handle(http.MethodGet, route1, handle1) 647 | router.Handle(http.MethodGet, route2, handle2) 648 | router.Handle(http.MethodGet, route3, handle3) 649 | 650 | w := new(mockResponseWriter) 651 | r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) 652 | router.ServeHTTP(w, r) 653 | if !routed1 || routed2 || routed3 { 654 | t.Fatal("Routing failed!") 655 | } 656 | 657 | w = new(mockResponseWriter) 658 | r, _ = http.NewRequest(http.MethodGet, "/user/gopher/details", nil) 659 | router.ServeHTTP(w, r) 660 | if !routed2 || routed3 { 661 | t.Fatal("Routing failed!") 662 | } 663 | 664 | w = new(mockResponseWriter) 665 | r, _ = http.NewRequest(http.MethodGet, "/", nil) 666 | router.ServeHTTP(w, r) 667 | if !routed3 { 668 | t.Fatal("Routing failed!") 669 | } 670 | } 671 | 672 | type mockFileSystem struct { 673 | opened bool 674 | } 675 | 676 | func (mfs *mockFileSystem) Open(name string) (http.File, error) { 677 | mfs.opened = true 678 | return nil, errors.New("this is just a mock") 679 | } 680 | 681 | func TestRouterServeFiles(t *testing.T) { 682 | router := New() 683 | mfs := &mockFileSystem{} 684 | 685 | recv := catchPanic(func() { 686 | router.ServeFiles("/noFilepath", mfs) 687 | }) 688 | if recv == nil { 689 | t.Fatal("registering path not ending with '*filepath' did not panic") 690 | } 691 | 692 | router.ServeFiles("/*filepath", mfs) 693 | w := new(mockResponseWriter) 694 | r, _ := http.NewRequest(http.MethodGet, "/favicon.ico", nil) 695 | router.ServeHTTP(w, r) 696 | if !mfs.opened { 697 | t.Error("serving file failed") 698 | } 699 | } 700 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | func min(a, b int) int { 14 | if a <= b { 15 | return a 16 | } 17 | return b 18 | } 19 | 20 | func longestCommonPrefix(a, b string) int { 21 | i := 0 22 | max := min(len(a), len(b)) 23 | for i < max && a[i] == b[i] { 24 | i++ 25 | } 26 | return i 27 | } 28 | 29 | // Search for a wildcard segment and check the name for invalid characters. 30 | // Returns -1 as index, if no wildcard was found. 31 | func findWildcard(path string) (wilcard string, i int, valid bool) { 32 | // Find start 33 | for start, c := range []byte(path) { 34 | // A wildcard starts with ':' (param) or '*' (catch-all) 35 | if c != ':' && c != '*' { 36 | continue 37 | } 38 | 39 | // Find end and check for invalid characters 40 | valid = true 41 | for end, c := range []byte(path[start+1:]) { 42 | switch c { 43 | case '/': 44 | return path[start : start+1+end], start, valid 45 | case ':', '*': 46 | valid = false 47 | } 48 | } 49 | return path[start:], start, valid 50 | } 51 | return "", -1, false 52 | } 53 | 54 | func countParams(path string) uint16 { 55 | var n uint16 56 | for i := range []byte(path) { 57 | switch path[i] { 58 | case ':', '*': 59 | n++ 60 | } 61 | } 62 | return n 63 | } 64 | 65 | type nodeType uint8 66 | 67 | const ( 68 | static nodeType = iota // default 69 | root 70 | param 71 | catchAll 72 | ) 73 | 74 | type node struct { 75 | path string 76 | indices string 77 | wildChild bool 78 | nType nodeType 79 | priority uint32 80 | children []*node 81 | handle Handle 82 | } 83 | 84 | // Increments priority of the given child and reorders if necessary 85 | func (n *node) incrementChildPrio(pos int) int { 86 | cs := n.children 87 | cs[pos].priority++ 88 | prio := cs[pos].priority 89 | 90 | // Adjust position (move to front) 91 | newPos := pos 92 | for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { 93 | // Swap node positions 94 | cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] 95 | } 96 | 97 | // Build new index char string 98 | if newPos != pos { 99 | n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty 100 | n.indices[pos:pos+1] + // The index char we move 101 | n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos' 102 | } 103 | 104 | return newPos 105 | } 106 | 107 | // addRoute adds a node with the given handle to the path. 108 | // Not concurrency-safe! 109 | func (n *node) addRoute(path string, handle Handle) { 110 | fullPath := path 111 | n.priority++ 112 | 113 | // Empty tree 114 | if n.path == "" && n.indices == "" { 115 | n.insertChild(path, fullPath, handle) 116 | n.nType = root 117 | return 118 | } 119 | 120 | walk: 121 | for { 122 | // Find the longest common prefix. 123 | // This also implies that the common prefix contains no ':' or '*' 124 | // since the existing key can't contain those chars. 125 | i := longestCommonPrefix(path, n.path) 126 | 127 | // Split edge 128 | if i < len(n.path) { 129 | child := node{ 130 | path: n.path[i:], 131 | wildChild: n.wildChild, 132 | nType: static, 133 | indices: n.indices, 134 | children: n.children, 135 | handle: n.handle, 136 | priority: n.priority - 1, 137 | } 138 | 139 | n.children = []*node{&child} 140 | // []byte for proper unicode char conversion, see #65 141 | n.indices = string([]byte{n.path[i]}) 142 | n.path = path[:i] 143 | n.handle = nil 144 | n.wildChild = false 145 | } 146 | 147 | // Make new node a child of this node 148 | if i < len(path) { 149 | path = path[i:] 150 | 151 | if n.wildChild { 152 | n = n.children[0] 153 | n.priority++ 154 | 155 | // Check if the wildcard matches 156 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] && 157 | // Adding a child to a catchAll is not possible 158 | n.nType != catchAll && 159 | // Check for longer wildcard, e.g. :name and :names 160 | (len(n.path) >= len(path) || path[len(n.path)] == '/') { 161 | continue walk 162 | } else { 163 | // Wildcard conflict 164 | pathSeg := path 165 | if n.nType != catchAll { 166 | pathSeg = strings.SplitN(pathSeg, "/", 2)[0] 167 | } 168 | prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path 169 | panic("'" + pathSeg + 170 | "' in new path '" + fullPath + 171 | "' conflicts with existing wildcard '" + n.path + 172 | "' in existing prefix '" + prefix + 173 | "'") 174 | } 175 | } 176 | 177 | idxc := path[0] 178 | 179 | // '/' after param 180 | if n.nType == param && idxc == '/' && len(n.children) == 1 { 181 | n = n.children[0] 182 | n.priority++ 183 | continue walk 184 | } 185 | 186 | // Check if a child with the next path byte exists 187 | for i, c := range []byte(n.indices) { 188 | if c == idxc { 189 | i = n.incrementChildPrio(i) 190 | n = n.children[i] 191 | continue walk 192 | } 193 | } 194 | 195 | // Otherwise insert it 196 | if idxc != ':' && idxc != '*' { 197 | // []byte for proper unicode char conversion, see #65 198 | n.indices += string([]byte{idxc}) 199 | child := &node{} 200 | n.children = append(n.children, child) 201 | n.incrementChildPrio(len(n.indices) - 1) 202 | n = child 203 | } 204 | n.insertChild(path, fullPath, handle) 205 | return 206 | } 207 | 208 | // Otherwise add handle to current node 209 | if n.handle != nil { 210 | panic("a handle is already registered for path '" + fullPath + "'") 211 | } 212 | n.handle = handle 213 | return 214 | } 215 | } 216 | 217 | func (n *node) insertChild(path, fullPath string, handle Handle) { 218 | for { 219 | // Find prefix until first wildcard 220 | wildcard, i, valid := findWildcard(path) 221 | if i < 0 { // No wilcard found 222 | break 223 | } 224 | 225 | // The wildcard name must not contain ':' and '*' 226 | if !valid { 227 | panic("only one wildcard per path segment is allowed, has: '" + 228 | wildcard + "' in path '" + fullPath + "'") 229 | } 230 | 231 | // Check if the wildcard has a name 232 | if len(wildcard) < 2 { 233 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 234 | } 235 | 236 | // Check if this node has existing children which would be 237 | // unreachable if we insert the wildcard here 238 | if len(n.children) > 0 { 239 | panic("wildcard segment '" + wildcard + 240 | "' conflicts with existing children in path '" + fullPath + "'") 241 | } 242 | 243 | // param 244 | if wildcard[0] == ':' { 245 | if i > 0 { 246 | // Insert prefix before the current wildcard 247 | n.path = path[:i] 248 | path = path[i:] 249 | } 250 | 251 | n.wildChild = true 252 | child := &node{ 253 | nType: param, 254 | path: wildcard, 255 | } 256 | n.children = []*node{child} 257 | n = child 258 | n.priority++ 259 | 260 | // If the path doesn't end with the wildcard, then there 261 | // will be another non-wildcard subpath starting with '/' 262 | if len(wildcard) < len(path) { 263 | path = path[len(wildcard):] 264 | child := &node{ 265 | priority: 1, 266 | } 267 | n.children = []*node{child} 268 | n = child 269 | continue 270 | } 271 | 272 | // Otherwise we're done. Insert the handle in the new leaf 273 | n.handle = handle 274 | return 275 | } 276 | 277 | // catchAll 278 | if i+len(wildcard) != len(path) { 279 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 280 | } 281 | 282 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 283 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 284 | } 285 | 286 | // Currently fixed width 1 for '/' 287 | i-- 288 | if path[i] != '/' { 289 | panic("no / before catch-all in path '" + fullPath + "'") 290 | } 291 | 292 | n.path = path[:i] 293 | 294 | // First node: catchAll node with empty path 295 | child := &node{ 296 | wildChild: true, 297 | nType: catchAll, 298 | } 299 | n.children = []*node{child} 300 | n.indices = string('/') 301 | n = child 302 | n.priority++ 303 | 304 | // Second node: node holding the variable 305 | child = &node{ 306 | path: path[i:], 307 | nType: catchAll, 308 | handle: handle, 309 | priority: 1, 310 | } 311 | n.children = []*node{child} 312 | 313 | return 314 | } 315 | 316 | // If no wildcard was found, simply insert the path and handle 317 | n.path = path 318 | n.handle = handle 319 | } 320 | 321 | // Returns the handle registered with the given path (key). The values of 322 | // wildcards are saved to a map. 323 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is 324 | // made if a handle exists with an extra (without the) trailing slash for the 325 | // given path. 326 | func (n *node) getValue(path string, params func() *Params) (handle Handle, ps *Params, tsr bool) { 327 | walk: // Outer loop for walking the tree 328 | for { 329 | prefix := n.path 330 | if len(path) > len(prefix) { 331 | if path[:len(prefix)] == prefix { 332 | path = path[len(prefix):] 333 | 334 | // If this node does not have a wildcard (param or catchAll) 335 | // child, we can just look up the next child node and continue 336 | // to walk down the tree 337 | if !n.wildChild { 338 | idxc := path[0] 339 | for i, c := range []byte(n.indices) { 340 | if c == idxc { 341 | n = n.children[i] 342 | continue walk 343 | } 344 | } 345 | 346 | // Nothing found. 347 | // We can recommend to redirect to the same URL without a 348 | // trailing slash if a leaf exists for that path. 349 | tsr = (path == "/" && n.handle != nil) 350 | return 351 | } 352 | 353 | // Handle wildcard child 354 | n = n.children[0] 355 | switch n.nType { 356 | case param: 357 | // Find param end (either '/' or path end) 358 | end := 0 359 | for end < len(path) && path[end] != '/' { 360 | end++ 361 | } 362 | 363 | // Save param value 364 | if params != nil { 365 | if ps == nil { 366 | ps = params() 367 | } 368 | // Expand slice within preallocated capacity 369 | i := len(*ps) 370 | *ps = (*ps)[:i+1] 371 | (*ps)[i] = Param{ 372 | Key: n.path[1:], 373 | Value: path[:end], 374 | } 375 | } 376 | 377 | // We need to go deeper! 378 | if end < len(path) { 379 | if len(n.children) > 0 { 380 | path = path[end:] 381 | n = n.children[0] 382 | continue walk 383 | } 384 | 385 | // ... but we can't 386 | tsr = (len(path) == end+1) 387 | return 388 | } 389 | 390 | if handle = n.handle; handle != nil { 391 | return 392 | } else if len(n.children) == 1 { 393 | // No handle found. Check if a handle for this path + a 394 | // trailing slash exists for TSR recommendation 395 | n = n.children[0] 396 | tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/") 397 | } 398 | 399 | return 400 | 401 | case catchAll: 402 | // Save param value 403 | if params != nil { 404 | if ps == nil { 405 | ps = params() 406 | } 407 | // Expand slice within preallocated capacity 408 | i := len(*ps) 409 | *ps = (*ps)[:i+1] 410 | (*ps)[i] = Param{ 411 | Key: n.path[2:], 412 | Value: path, 413 | } 414 | } 415 | 416 | handle = n.handle 417 | return 418 | 419 | default: 420 | panic("invalid node type") 421 | } 422 | } 423 | } else if path == prefix { 424 | // We should have reached the node containing the handle. 425 | // Check if this node has a handle registered. 426 | if handle = n.handle; handle != nil { 427 | return 428 | } 429 | 430 | // If there is no handle for this route, but this route has a 431 | // wildcard child, there must be a handle for this path with an 432 | // additional trailing slash 433 | if path == "/" && n.wildChild && n.nType != root { 434 | tsr = true 435 | return 436 | } 437 | 438 | if path == "/" && n.nType == static { 439 | tsr = true 440 | return 441 | } 442 | 443 | // No handle found. Check if a handle for this path + a 444 | // trailing slash exists for trailing slash recommendation 445 | for i, c := range []byte(n.indices) { 446 | if c == '/' { 447 | n = n.children[i] 448 | tsr = (len(n.path) == 1 && n.handle != nil) || 449 | (n.nType == catchAll && n.children[0].handle != nil) 450 | return 451 | } 452 | } 453 | return 454 | } 455 | 456 | // Nothing found. We can recommend to redirect to the same URL with an 457 | // extra trailing slash if a leaf exists for that path 458 | tsr = (path == "/") || 459 | (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && 460 | path == prefix[:len(prefix)-1] && n.handle != nil) 461 | return 462 | } 463 | } 464 | 465 | // Makes a case-insensitive lookup of the given path and tries to find a handler. 466 | // It can optionally also fix trailing slashes. 467 | // It returns the case-corrected path and a bool indicating whether the lookup 468 | // was successful. 469 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) { 470 | const stackBufSize = 128 471 | 472 | // Use a static sized buffer on the stack in the common case. 473 | // If the path is too long, allocate a buffer on the heap instead. 474 | buf := make([]byte, 0, stackBufSize) 475 | if l := len(path) + 1; l > stackBufSize { 476 | buf = make([]byte, 0, l) 477 | } 478 | 479 | ciPath := n.findCaseInsensitivePathRec( 480 | path, 481 | buf, // Preallocate enough memory for new path 482 | [4]byte{}, // Empty rune buffer 483 | fixTrailingSlash, 484 | ) 485 | 486 | return string(ciPath), ciPath != nil 487 | } 488 | 489 | // Shift bytes in array by n bytes left 490 | func shiftNRuneBytes(rb [4]byte, n int) [4]byte { 491 | switch n { 492 | case 0: 493 | return rb 494 | case 1: 495 | return [4]byte{rb[1], rb[2], rb[3], 0} 496 | case 2: 497 | return [4]byte{rb[2], rb[3]} 498 | case 3: 499 | return [4]byte{rb[3]} 500 | default: 501 | return [4]byte{} 502 | } 503 | } 504 | 505 | // Recursive case-insensitive lookup function used by n.findCaseInsensitivePath 506 | func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { 507 | npLen := len(n.path) 508 | 509 | walk: // Outer loop for walking the tree 510 | for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { 511 | // Add common prefix to result 512 | oldPath := path 513 | path = path[npLen:] 514 | ciPath = append(ciPath, n.path...) 515 | 516 | if len(path) > 0 { 517 | // If this node does not have a wildcard (param or catchAll) child, 518 | // we can just look up the next child node and continue to walk down 519 | // the tree 520 | if !n.wildChild { 521 | // Skip rune bytes already processed 522 | rb = shiftNRuneBytes(rb, npLen) 523 | 524 | if rb[0] != 0 { 525 | // Old rune not finished 526 | idxc := rb[0] 527 | for i, c := range []byte(n.indices) { 528 | if c == idxc { 529 | // continue with child node 530 | n = n.children[i] 531 | npLen = len(n.path) 532 | continue walk 533 | } 534 | } 535 | } else { 536 | // Process a new rune 537 | var rv rune 538 | 539 | // Find rune start. 540 | // Runes are up to 4 byte long, 541 | // -4 would definitely be another rune. 542 | var off int 543 | for max := min(npLen, 3); off < max; off++ { 544 | if i := npLen - off; utf8.RuneStart(oldPath[i]) { 545 | // read rune from cached path 546 | rv, _ = utf8.DecodeRuneInString(oldPath[i:]) 547 | break 548 | } 549 | } 550 | 551 | // Calculate lowercase bytes of current rune 552 | lo := unicode.ToLower(rv) 553 | utf8.EncodeRune(rb[:], lo) 554 | 555 | // Skip already processed bytes 556 | rb = shiftNRuneBytes(rb, off) 557 | 558 | idxc := rb[0] 559 | for i, c := range []byte(n.indices) { 560 | // Lowercase matches 561 | if c == idxc { 562 | // must use a recursive approach since both the 563 | // uppercase byte and the lowercase byte might exist 564 | // as an index 565 | if out := n.children[i].findCaseInsensitivePathRec( 566 | path, ciPath, rb, fixTrailingSlash, 567 | ); out != nil { 568 | return out 569 | } 570 | break 571 | } 572 | } 573 | 574 | // If we found no match, the same for the uppercase rune, 575 | // if it differs 576 | if up := unicode.ToUpper(rv); up != lo { 577 | utf8.EncodeRune(rb[:], up) 578 | rb = shiftNRuneBytes(rb, off) 579 | 580 | idxc := rb[0] 581 | for i, c := range []byte(n.indices) { 582 | // Uppercase matches 583 | if c == idxc { 584 | // Continue with child node 585 | n = n.children[i] 586 | npLen = len(n.path) 587 | continue walk 588 | } 589 | } 590 | } 591 | } 592 | 593 | // Nothing found. We can recommend to redirect to the same URL 594 | // without a trailing slash if a leaf exists for that path 595 | if fixTrailingSlash && path == "/" && n.handle != nil { 596 | return ciPath 597 | } 598 | return nil 599 | } 600 | 601 | n = n.children[0] 602 | switch n.nType { 603 | case param: 604 | // Find param end (either '/' or path end) 605 | end := 0 606 | for end < len(path) && path[end] != '/' { 607 | end++ 608 | } 609 | 610 | // Add param value to case insensitive path 611 | ciPath = append(ciPath, path[:end]...) 612 | 613 | // We need to go deeper! 614 | if end < len(path) { 615 | if len(n.children) > 0 { 616 | // Continue with child node 617 | n = n.children[0] 618 | npLen = len(n.path) 619 | path = path[end:] 620 | continue 621 | } 622 | 623 | // ... but we can't 624 | if fixTrailingSlash && len(path) == end+1 { 625 | return ciPath 626 | } 627 | return nil 628 | } 629 | 630 | if n.handle != nil { 631 | return ciPath 632 | } else if fixTrailingSlash && len(n.children) == 1 { 633 | // No handle found. Check if a handle for this path + a 634 | // trailing slash exists 635 | n = n.children[0] 636 | if n.path == "/" && n.handle != nil { 637 | return append(ciPath, '/') 638 | } 639 | } 640 | return nil 641 | 642 | case catchAll: 643 | return append(ciPath, path...) 644 | 645 | default: 646 | panic("invalid node type") 647 | } 648 | } else { 649 | // We should have reached the node containing the handle. 650 | // Check if this node has a handle registered. 651 | if n.handle != nil { 652 | return ciPath 653 | } 654 | 655 | // No handle found. 656 | // Try to fix the path by adding a trailing slash 657 | if fixTrailingSlash { 658 | for i, c := range []byte(n.indices) { 659 | if c == '/' { 660 | n = n.children[i] 661 | if (len(n.path) == 1 && n.handle != nil) || 662 | (n.nType == catchAll && n.children[0].handle != nil) { 663 | return append(ciPath, '/') 664 | } 665 | return nil 666 | } 667 | } 668 | } 669 | return nil 670 | } 671 | } 672 | 673 | // Nothing found. 674 | // Try to fix the path by adding / removing a trailing slash 675 | if fixTrailingSlash { 676 | if path == "/" { 677 | return ciPath 678 | } 679 | if len(path)+1 == npLen && n.path[len(path)] == '/' && 680 | strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { 681 | return append(ciPath, n.path...) 682 | } 683 | } 684 | return nil 685 | } 686 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | // func printChildren(n *node, prefix string) { 17 | // fmt.Printf(" %02d %s%s[%d] %v %t %d \r\n", n.priority, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) 18 | // for l := len(n.path); l > 0; l-- { 19 | // prefix += " " 20 | // } 21 | // for _, child := range n.children { 22 | // printChildren(child, prefix) 23 | // } 24 | // } 25 | 26 | // Used as a workaround since we can't compare functions or their addresses 27 | var fakeHandlerValue string 28 | 29 | func fakeHandler(val string) Handle { 30 | return func(http.ResponseWriter, *http.Request, Params) { 31 | fakeHandlerValue = val 32 | } 33 | } 34 | 35 | type testRequests []struct { 36 | path string 37 | nilHandler bool 38 | route string 39 | ps Params 40 | } 41 | 42 | func getParams() *Params { 43 | ps := make(Params, 0, 20) 44 | return &ps 45 | } 46 | 47 | func checkRequests(t *testing.T, tree *node, requests testRequests) { 48 | for _, request := range requests { 49 | handler, psp, _ := tree.getValue(request.path, getParams) 50 | 51 | switch { 52 | case handler == nil: 53 | if !request.nilHandler { 54 | t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) 55 | } 56 | case request.nilHandler: 57 | t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) 58 | default: 59 | handler(nil, nil, nil) 60 | if fakeHandlerValue != request.route { 61 | t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) 62 | } 63 | } 64 | 65 | var ps Params 66 | if psp != nil { 67 | ps = *psp 68 | } 69 | 70 | if !reflect.DeepEqual(ps, request.ps) { 71 | t.Errorf("Params mismatch for route '%s'", request.path) 72 | } 73 | } 74 | } 75 | 76 | func checkPriorities(t *testing.T, n *node) uint32 { 77 | var prio uint32 78 | for i := range n.children { 79 | prio += checkPriorities(t, n.children[i]) 80 | } 81 | 82 | if n.handle != nil { 83 | prio++ 84 | } 85 | 86 | if n.priority != prio { 87 | t.Errorf( 88 | "priority mismatch for node '%s': is %d, should be %d", 89 | n.path, n.priority, prio, 90 | ) 91 | } 92 | 93 | return prio 94 | } 95 | 96 | func TestCountParams(t *testing.T) { 97 | if countParams("/path/:param1/static/*catch-all") != 2 { 98 | t.Fail() 99 | } 100 | if countParams(strings.Repeat("/:param", 256)) != 256 { 101 | t.Fail() 102 | } 103 | } 104 | 105 | func TestTreeAddAndGet(t *testing.T) { 106 | tree := &node{} 107 | 108 | routes := [...]string{ 109 | "/hi", 110 | "/contact", 111 | "/co", 112 | "/c", 113 | "/a", 114 | "/ab", 115 | "/doc/", 116 | "/doc/go_faq.html", 117 | "/doc/go1.html", 118 | "/α", 119 | "/β", 120 | } 121 | for _, route := range routes { 122 | tree.addRoute(route, fakeHandler(route)) 123 | } 124 | 125 | // printChildren(tree, "") 126 | 127 | checkRequests(t, tree, testRequests{ 128 | {"/a", false, "/a", nil}, 129 | {"/", true, "", nil}, 130 | {"/hi", false, "/hi", nil}, 131 | {"/contact", false, "/contact", nil}, 132 | {"/co", false, "/co", nil}, 133 | {"/con", true, "", nil}, // key mismatch 134 | {"/cona", true, "", nil}, // key mismatch 135 | {"/no", true, "", nil}, // no matching child 136 | {"/ab", false, "/ab", nil}, 137 | {"/α", false, "/α", nil}, 138 | {"/β", false, "/β", nil}, 139 | }) 140 | 141 | checkPriorities(t, tree) 142 | } 143 | 144 | func TestTreeWildcard(t *testing.T) { 145 | tree := &node{} 146 | 147 | routes := [...]string{ 148 | "/", 149 | "/cmd/:tool/:sub", 150 | "/cmd/:tool/", 151 | "/src/*filepath", 152 | "/search/", 153 | "/search/:query", 154 | "/user_:name", 155 | "/user_:name/about", 156 | "/files/:dir/*filepath", 157 | "/doc/", 158 | "/doc/go_faq.html", 159 | "/doc/go1.html", 160 | "/info/:user/public", 161 | "/info/:user/project/:project", 162 | } 163 | for _, route := range routes { 164 | tree.addRoute(route, fakeHandler(route)) 165 | } 166 | 167 | // printChildren(tree, "") 168 | 169 | checkRequests(t, tree, testRequests{ 170 | {"/", false, "/", nil}, 171 | {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, 172 | {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, 173 | {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, 174 | {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, 175 | {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, 176 | {"/search/", false, "/search/", nil}, 177 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 178 | {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 179 | {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, 180 | {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, 181 | {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, 182 | {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, 183 | {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, 184 | }) 185 | 186 | checkPriorities(t, tree) 187 | } 188 | 189 | func catchPanic(testFunc func()) (recv interface{}) { 190 | defer func() { 191 | recv = recover() 192 | }() 193 | 194 | testFunc() 195 | return 196 | } 197 | 198 | type testRoute struct { 199 | path string 200 | conflict bool 201 | } 202 | 203 | func testRoutes(t *testing.T, routes []testRoute) { 204 | tree := &node{} 205 | 206 | for i := range routes { 207 | route := routes[i] 208 | recv := catchPanic(func() { 209 | tree.addRoute(route.path, nil) 210 | }) 211 | 212 | if route.conflict { 213 | if recv == nil { 214 | t.Errorf("no panic for conflicting route '%s'", route.path) 215 | } 216 | } else if recv != nil { 217 | t.Errorf("unexpected panic for route '%s': %v", route.path, recv) 218 | } 219 | } 220 | 221 | // printChildren(tree, "") 222 | } 223 | 224 | func TestTreeWildcardConflict(t *testing.T) { 225 | routes := []testRoute{ 226 | {"/cmd/:tool/:sub", false}, 227 | {"/cmd/vet", true}, 228 | {"/src/*filepath", false}, 229 | {"/src/*filepathx", true}, 230 | {"/src/", true}, 231 | {"/src1/", false}, 232 | {"/src1/*filepath", true}, 233 | {"/src2*filepath", true}, 234 | {"/search/:query", false}, 235 | {"/search/invalid", true}, 236 | {"/user_:name", false}, 237 | {"/user_x", true}, 238 | {"/user_:name", false}, 239 | {"/id:id", false}, 240 | {"/id/:id", true}, 241 | } 242 | testRoutes(t, routes) 243 | } 244 | 245 | func TestTreeChildConflict(t *testing.T) { 246 | routes := []testRoute{ 247 | {"/cmd/vet", false}, 248 | {"/cmd/:tool/:sub", true}, 249 | {"/src/AUTHORS", false}, 250 | {"/src/*filepath", true}, 251 | {"/user_x", false}, 252 | {"/user_:name", true}, 253 | {"/id/:id", false}, 254 | {"/id:id", true}, 255 | {"/:id", true}, 256 | {"/*filepath", true}, 257 | } 258 | testRoutes(t, routes) 259 | } 260 | 261 | func TestTreeDupliatePath(t *testing.T) { 262 | tree := &node{} 263 | 264 | routes := [...]string{ 265 | "/", 266 | "/doc/", 267 | "/src/*filepath", 268 | "/search/:query", 269 | "/user_:name", 270 | } 271 | for i := range routes { 272 | route := routes[i] 273 | recv := catchPanic(func() { 274 | tree.addRoute(route, fakeHandler(route)) 275 | }) 276 | if recv != nil { 277 | t.Fatalf("panic inserting route '%s': %v", route, recv) 278 | } 279 | 280 | // Add again 281 | recv = catchPanic(func() { 282 | tree.addRoute(route, nil) 283 | }) 284 | if recv == nil { 285 | t.Fatalf("no panic while inserting duplicate route '%s", route) 286 | } 287 | } 288 | 289 | // printChildren(tree, "") 290 | 291 | checkRequests(t, tree, testRequests{ 292 | {"/", false, "/", nil}, 293 | {"/doc/", false, "/doc/", nil}, 294 | {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, 295 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 296 | {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, 297 | }) 298 | } 299 | 300 | func TestEmptyWildcardName(t *testing.T) { 301 | tree := &node{} 302 | 303 | routes := [...]string{ 304 | "/user:", 305 | "/user:/", 306 | "/cmd/:/", 307 | "/src/*", 308 | } 309 | for i := range routes { 310 | route := routes[i] 311 | recv := catchPanic(func() { 312 | tree.addRoute(route, nil) 313 | }) 314 | if recv == nil { 315 | t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) 316 | } 317 | } 318 | } 319 | 320 | func TestTreeCatchAllConflict(t *testing.T) { 321 | routes := []testRoute{ 322 | {"/src/*filepath/x", true}, 323 | {"/src2/", false}, 324 | {"/src2/*filepath/x", true}, 325 | {"/src3/*filepath", false}, 326 | {"/src3/*filepath/x", true}, 327 | } 328 | testRoutes(t, routes) 329 | } 330 | 331 | func TestTreeCatchAllConflictRoot(t *testing.T) { 332 | routes := []testRoute{ 333 | {"/", false}, 334 | {"/*filepath", true}, 335 | } 336 | testRoutes(t, routes) 337 | } 338 | 339 | func TestTreeCatchMaxParams(t *testing.T) { 340 | tree := &node{} 341 | var route = "/cmd/*filepath" 342 | tree.addRoute(route, fakeHandler(route)) 343 | } 344 | 345 | func TestTreeDoubleWildcard(t *testing.T) { 346 | const panicMsg = "only one wildcard per path segment is allowed" 347 | 348 | routes := [...]string{ 349 | "/:foo:bar", 350 | "/:foo:bar/", 351 | "/:foo*bar", 352 | } 353 | 354 | for i := range routes { 355 | route := routes[i] 356 | tree := &node{} 357 | recv := catchPanic(func() { 358 | tree.addRoute(route, nil) 359 | }) 360 | 361 | if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { 362 | t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) 363 | } 364 | } 365 | } 366 | 367 | func TestTreeTrailingSlashRedirect(t *testing.T) { 368 | tree := &node{} 369 | 370 | routes := [...]string{ 371 | "/hi", 372 | "/b/", 373 | "/search/:query", 374 | "/cmd/:tool/", 375 | "/src/*filepath", 376 | "/x", 377 | "/x/y", 378 | "/y/", 379 | "/y/z", 380 | "/0/:id", 381 | "/0/:id/1", 382 | "/1/:id/", 383 | "/1/:id/2", 384 | "/aa", 385 | "/a/", 386 | "/admin", 387 | "/admin/:category", 388 | "/admin/:category/:page", 389 | "/doc", 390 | "/doc/go_faq.html", 391 | "/doc/go1.html", 392 | "/no/a", 393 | "/no/b", 394 | "/api/hello/:name", 395 | "/vendor/:x/*y", 396 | } 397 | for i := range routes { 398 | route := routes[i] 399 | recv := catchPanic(func() { 400 | tree.addRoute(route, fakeHandler(route)) 401 | }) 402 | if recv != nil { 403 | t.Fatalf("panic inserting route '%s': %v", route, recv) 404 | } 405 | } 406 | 407 | // printChildren(tree, "") 408 | 409 | tsrRoutes := [...]string{ 410 | "/hi/", 411 | "/b", 412 | "/search/gopher/", 413 | "/cmd/vet", 414 | "/src", 415 | "/x/", 416 | "/y", 417 | "/0/go/", 418 | "/1/go", 419 | "/a", 420 | "/admin/", 421 | "/admin/config/", 422 | "/admin/config/permissions/", 423 | "/doc/", 424 | "/vendor/x", 425 | } 426 | for _, route := range tsrRoutes { 427 | handler, _, tsr := tree.getValue(route, nil) 428 | if handler != nil { 429 | t.Fatalf("non-nil handler for TSR route '%s", route) 430 | } else if !tsr { 431 | t.Errorf("expected TSR recommendation for route '%s'", route) 432 | } 433 | } 434 | 435 | noTsrRoutes := [...]string{ 436 | "/", 437 | "/no", 438 | "/no/", 439 | "/_", 440 | "/_/", 441 | "/api/world/abc", 442 | } 443 | for _, route := range noTsrRoutes { 444 | handler, _, tsr := tree.getValue(route, nil) 445 | if handler != nil { 446 | t.Fatalf("non-nil handler for No-TSR route '%s", route) 447 | } else if tsr { 448 | t.Errorf("expected no TSR recommendation for route '%s'", route) 449 | } 450 | } 451 | } 452 | 453 | func TestTreeRootTrailingSlashRedirect(t *testing.T) { 454 | tree := &node{} 455 | 456 | recv := catchPanic(func() { 457 | tree.addRoute("/:test", fakeHandler("/:test")) 458 | }) 459 | if recv != nil { 460 | t.Fatalf("panic inserting test route: %v", recv) 461 | } 462 | 463 | handler, _, tsr := tree.getValue("/", nil) 464 | if handler != nil { 465 | t.Fatalf("non-nil handler") 466 | } else if tsr { 467 | t.Errorf("expected no TSR recommendation") 468 | } 469 | } 470 | 471 | func TestTreeFindCaseInsensitivePath(t *testing.T) { 472 | tree := &node{} 473 | 474 | longPath := "/l" + strings.Repeat("o", 128) + "ng" 475 | lOngPath := "/l" + strings.Repeat("O", 128) + "ng/" 476 | 477 | routes := [...]string{ 478 | "/hi", 479 | "/b/", 480 | "/ABC/", 481 | "/search/:query", 482 | "/cmd/:tool/", 483 | "/src/*filepath", 484 | "/x", 485 | "/x/y", 486 | "/y/", 487 | "/y/z", 488 | "/0/:id", 489 | "/0/:id/1", 490 | "/1/:id/", 491 | "/1/:id/2", 492 | "/aa", 493 | "/a/", 494 | "/doc", 495 | "/doc/go_faq.html", 496 | "/doc/go1.html", 497 | "/doc/go/away", 498 | "/no/a", 499 | "/no/b", 500 | "/Π", 501 | "/u/apfêl/", 502 | "/u/äpfêl/", 503 | "/u/öpfêl", 504 | "/v/Äpfêl/", 505 | "/v/Öpfêl", 506 | "/w/♬", // 3 byte 507 | "/w/♭/", // 3 byte, last byte differs 508 | "/w/𠜎", // 4 byte 509 | "/w/𠜏/", // 4 byte 510 | longPath, 511 | } 512 | 513 | for i := range routes { 514 | route := routes[i] 515 | recv := catchPanic(func() { 516 | tree.addRoute(route, fakeHandler(route)) 517 | }) 518 | if recv != nil { 519 | t.Fatalf("panic inserting route '%s': %v", route, recv) 520 | } 521 | } 522 | 523 | // Check out == in for all registered routes 524 | // With fixTrailingSlash = true 525 | for i := range routes { 526 | route := routes[i] 527 | out, found := tree.findCaseInsensitivePath(route, true) 528 | if !found { 529 | t.Errorf("Route '%s' not found!", route) 530 | } else if out != route { 531 | t.Errorf("Wrong result for route '%s': %s", route, out) 532 | } 533 | } 534 | // With fixTrailingSlash = false 535 | for i := range routes { 536 | route := routes[i] 537 | out, found := tree.findCaseInsensitivePath(route, false) 538 | if !found { 539 | t.Errorf("Route '%s' not found!", route) 540 | } else if out != route { 541 | t.Errorf("Wrong result for route '%s': %s", route, out) 542 | } 543 | } 544 | 545 | tests := []struct { 546 | in string 547 | out string 548 | found bool 549 | slash bool 550 | }{ 551 | {"/HI", "/hi", true, false}, 552 | {"/HI/", "/hi", true, true}, 553 | {"/B", "/b/", true, true}, 554 | {"/B/", "/b/", true, false}, 555 | {"/abc", "/ABC/", true, true}, 556 | {"/abc/", "/ABC/", true, false}, 557 | {"/aBc", "/ABC/", true, true}, 558 | {"/aBc/", "/ABC/", true, false}, 559 | {"/abC", "/ABC/", true, true}, 560 | {"/abC/", "/ABC/", true, false}, 561 | {"/SEARCH/QUERY", "/search/QUERY", true, false}, 562 | {"/SEARCH/QUERY/", "/search/QUERY", true, true}, 563 | {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, 564 | {"/CMD/TOOL", "/cmd/TOOL/", true, true}, 565 | {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, 566 | {"/x/Y", "/x/y", true, false}, 567 | {"/x/Y/", "/x/y", true, true}, 568 | {"/X/y", "/x/y", true, false}, 569 | {"/X/y/", "/x/y", true, true}, 570 | {"/X/Y", "/x/y", true, false}, 571 | {"/X/Y/", "/x/y", true, true}, 572 | {"/Y/", "/y/", true, false}, 573 | {"/Y", "/y/", true, true}, 574 | {"/Y/z", "/y/z", true, false}, 575 | {"/Y/z/", "/y/z", true, true}, 576 | {"/Y/Z", "/y/z", true, false}, 577 | {"/Y/Z/", "/y/z", true, true}, 578 | {"/y/Z", "/y/z", true, false}, 579 | {"/y/Z/", "/y/z", true, true}, 580 | {"/Aa", "/aa", true, false}, 581 | {"/Aa/", "/aa", true, true}, 582 | {"/AA", "/aa", true, false}, 583 | {"/AA/", "/aa", true, true}, 584 | {"/aA", "/aa", true, false}, 585 | {"/aA/", "/aa", true, true}, 586 | {"/A/", "/a/", true, false}, 587 | {"/A", "/a/", true, true}, 588 | {"/DOC", "/doc", true, false}, 589 | {"/DOC/", "/doc", true, true}, 590 | {"/NO", "", false, true}, 591 | {"/DOC/GO", "", false, true}, 592 | {"/π", "/Π", true, false}, 593 | {"/π/", "/Π", true, true}, 594 | {"/u/ÄPFÊL/", "/u/äpfêl/", true, false}, 595 | {"/u/ÄPFÊL", "/u/äpfêl/", true, true}, 596 | {"/u/ÖPFÊL/", "/u/öpfêl", true, true}, 597 | {"/u/ÖPFÊL", "/u/öpfêl", true, false}, 598 | {"/v/äpfêL/", "/v/Äpfêl/", true, false}, 599 | {"/v/äpfêL", "/v/Äpfêl/", true, true}, 600 | {"/v/öpfêL/", "/v/Öpfêl", true, true}, 601 | {"/v/öpfêL", "/v/Öpfêl", true, false}, 602 | {"/w/♬/", "/w/♬", true, true}, 603 | {"/w/♭", "/w/♭/", true, true}, 604 | {"/w/𠜎/", "/w/𠜎", true, true}, 605 | {"/w/𠜏", "/w/𠜏/", true, true}, 606 | {lOngPath, longPath, true, true}, 607 | } 608 | // With fixTrailingSlash = true 609 | for _, test := range tests { 610 | out, found := tree.findCaseInsensitivePath(test.in, true) 611 | if found != test.found || (found && (out != test.out)) { 612 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 613 | test.in, out, found, test.out, test.found) 614 | return 615 | } 616 | } 617 | // With fixTrailingSlash = false 618 | for _, test := range tests { 619 | out, found := tree.findCaseInsensitivePath(test.in, false) 620 | if test.slash { 621 | if found { // test needs a trailingSlash fix. It must not be found! 622 | t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, out) 623 | } 624 | } else { 625 | if found != test.found || (found && (out != test.out)) { 626 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 627 | test.in, out, found, test.out, test.found) 628 | return 629 | } 630 | } 631 | } 632 | } 633 | 634 | func TestTreeInvalidNodeType(t *testing.T) { 635 | const panicMsg = "invalid node type" 636 | 637 | tree := &node{} 638 | tree.addRoute("/", fakeHandler("/")) 639 | tree.addRoute("/:page", fakeHandler("/:page")) 640 | 641 | // set invalid node type 642 | tree.children[0].nType = 42 643 | 644 | // normal lookup 645 | recv := catchPanic(func() { 646 | tree.getValue("/test", nil) 647 | }) 648 | if rs, ok := recv.(string); !ok || rs != panicMsg { 649 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 650 | } 651 | 652 | // case-insensitive lookup 653 | recv = catchPanic(func() { 654 | tree.findCaseInsensitivePath("/test", true) 655 | }) 656 | if rs, ok := recv.(string); !ok || rs != panicMsg { 657 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 658 | } 659 | } 660 | 661 | func TestTreeWildcardConflictEx(t *testing.T) { 662 | conflicts := [...]struct { 663 | route string 664 | segPath string 665 | existPath string 666 | existSegPath string 667 | }{ 668 | {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, 669 | {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, 670 | {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, 671 | {"/conxxx", "xxx", `/con:tact`, `:tact`}, 672 | {"/conooo/xxx", "ooo", `/con:tact`, `:tact`}, 673 | } 674 | 675 | for i := range conflicts { 676 | conflict := conflicts[i] 677 | 678 | // I have to re-create a 'tree', because the 'tree' will be 679 | // in an inconsistent state when the loop recovers from the 680 | // panic which threw by 'addRoute' function. 681 | tree := &node{} 682 | routes := [...]string{ 683 | "/con:tact", 684 | "/who/are/*you", 685 | "/who/foo/hello", 686 | } 687 | 688 | for i := range routes { 689 | route := routes[i] 690 | tree.addRoute(route, fakeHandler(route)) 691 | } 692 | 693 | recv := catchPanic(func() { 694 | tree.addRoute(conflict.route, fakeHandler(conflict.route)) 695 | }) 696 | 697 | if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) { 698 | t.Fatalf("invalid wildcard conflict error (%v)", recv) 699 | } 700 | } 701 | } 702 | 703 | func TestRedirectTrailingSlash(t *testing.T) { 704 | var data = []struct { 705 | path string 706 | }{ 707 | {"/hello/:name"}, 708 | {"/hello/:name/123"}, 709 | {"/hello/:name/234"}, 710 | } 711 | 712 | node := &node{} 713 | for _, item := range data { 714 | node.addRoute(item.path, fakeHandler("test")) 715 | } 716 | 717 | _, _, tsr := node.getValue("/hello/abx/", nil) 718 | if tsr != true { 719 | t.Fatalf("want true, is false") 720 | } 721 | } 722 | --------------------------------------------------------------------------------