├── .gitignore ├── .travis.yml ├── HttpRouterLicense ├── LICENSE ├── README.md ├── examples ├── auth │ ├── README.md │ └── auth.go ├── basic │ ├── README.md │ └── basic.go └── hosts │ ├── README.md │ └── hosts.go ├── path.go ├── path_test.go ├── router.go ├── router_test.go ├── tree.go └── tree_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | coverage.out 4 | examples/basic/basic 5 | examples/hosts/hosts 6 | examples/auth/auth 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | 4 | go: 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - tip 9 | 10 | before_install: 11 | - go get -v github.com/axw/gocov/gocov 12 | - go get -v github.com/mattn/goveralls 13 | # - go get -v github.com/golang/lint/golint 14 | 15 | install: 16 | - go get -d -t -v ./... 17 | - go install -v 18 | 19 | script: 20 | - go vet ./... 21 | # - $HOME/gopath/bin/golint ./... 22 | - go test -v -covermode=count -coverprofile=coverage.out 23 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 24 | 25 | -after_success: 26 | - coveralls -------------------------------------------------------------------------------- /HttpRouterLicense: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Julien Schmidt. All rights reserved. 2 | 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of the contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, 招牌疯子 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of uq nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastHttpRouter 2 | [![Build Status](https://travis-ci.org/buaazp/fasthttprouter.svg?branch=master)](https://travis-ci.org/buaazp/fasthttprouter) 3 | [![Coverage Status](https://coveralls.io/repos/buaazp/fasthttprouter/badge.svg?branch=master&service=github)](https://coveralls.io/github/buaazp/fasthttprouter?branch=master) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/buaazp/fasthttprouter)](https://goreportcard.com/report/github.com/buaazp/fasthttprouter) 5 | [![GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter?status.svg)](http://godoc.org/github.com/buaazp/fasthttprouter) 6 | [![GitHub release](https://img.shields.io/github/release/buaazp/fasthttprouter.svg)](https://github.com/buaazp/fasthttprouter/releases) 7 | 8 | FastHttpRouter is forked from [httprouter](https://github.com/julienschmidt/httprouter) which is a lightweight high performance HTTP request router 9 | (also called *multiplexer* or just *mux* for short) for [fasthttp](https://github.com/valyala/fasthttp). 10 | 11 | This 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. 12 | 13 | #### License Related 14 | 15 | - The author of `httprouter` [@julienschmidt](https://github.com/julienschmidt) did almost all the hard work of this router. 16 | - I respect the laws of open source. So LICENSE of `httprouter` is alway stay here: [HttpRouterLicense](HttpRouterLicense). 17 | - What I do is just fit for `fasthttp`. I have no hope to build a huge but toxic go web framwork like [iris](https://github.com/kataras/iris). 18 | - I fork this repo is just because there is no router for `fasthttp` at that time. And `fasthttprouter` is the FIRST router for `fasthttp`. 19 | - `fasthttprouter` has been used in my online production and processes 17 million requests per day. It is fast and stable, so I decide to release a stable version. 20 | 21 | #### Releases 22 | 23 | - [2016.10.24] [v0.1.0](https://github.com/buaazp/fasthttprouter/releases/tag/v0.1.0) The first release version of `fasthttprouter`. 24 | 25 | ## Features 26 | 27 | **Best Performance:** FastHttpRouter is **one of the fastest** go web frameworks in the [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark). Even faster than httprouter itself. 28 | 29 | - Basic Test: The first test case is to mock 0 ms, 10 ms, 100 ms, 500 ms processing time in handlers. The concurrency clients are 5000. 30 | 31 | ![](http://ww3.sinaimg.cn/large/4c422e03jw1f2p6nyqh9ij20mm0aktbj.jpg) 32 | 33 | - Concurrency Test: In 30 ms processing time, the test result for 100, 1000, 5000 clients is: 34 | 35 | ![](http://ww4.sinaimg.cn/large/4c422e03jw1f2p6o1cdbij20lk09sack.jpg) 36 | 37 | See below for technical details of the implementation. 38 | 39 | **Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux), 40 | a requested URL path could match multiple patterns. Therefore they have some 41 | awkward pattern priority rules, like *longest match* or *first registered, 42 | first matched*. By design of this router, a request can only match exactly one 43 | or no route. As a result, there are also no unintended matches, which makes it 44 | great for SEO and improves the user experience. 45 | 46 | **Stop caring about trailing slashes:** Choose the URL style you like, the 47 | router automatically redirects the client if a trailing slash is missing or if 48 | there is one extra. Of course it only does so, if the new path has a handler. 49 | If you don't like it, you can [turn off this behavior](http://godoc.org/github.com/buaazp/fasthttprouter#Router.RedirectTrailingSlash). 50 | 51 | **Path auto-correction:** Besides detecting the missing or additional trailing 52 | slash at no extra cost, the router can also fix wrong cases and remove 53 | superfluous path elements (like `../` or `//`). 54 | Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? 55 | FastHttpRouter can help him by making a case-insensitive look-up and redirecting him 56 | to the correct URL. 57 | 58 | **Parameters in your routing pattern:** Stop parsing the requested URL path, 59 | just give the path segment a name and the router delivers the dynamic value to 60 | you. Because of the design of the router, path parameters are very cheap. 61 | 62 | **Zero Garbage:** The matching and dispatching process generates zero bytes of 63 | garbage. In fact, the only heap allocations that are made, is by building the 64 | slice of the key-value pairs for path parameters. If the request path contains 65 | no parameters, not a single heap allocation is necessary. 66 | 67 | **No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/buaazp/fasthttprouter#Router.PanicHandler) to deal with panics 68 | occurring during handling a HTTP request. The router then recovers and lets the 69 | PanicHandler log what happened and deliver a nice error page. 70 | 71 | **Perfect for APIs:** The router design encourages to build sensible, hierarchical 72 | RESTful APIs. Moreover it has builtin native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html) 73 | and `405 Method Not Allowed` replies. 74 | 75 | Of course you can also set **custom [NotFound](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) and [MethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](http://godoc.org/github.com/buaazp/fasthttprouter#Router.ServeFiles). 76 | 77 | ## Usage 78 | 79 | This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter) for details: 80 | 81 | Let's start with a trivial example: 82 | 83 | ```go 84 | package main 85 | 86 | import ( 87 | "fmt" 88 | "log" 89 | 90 | "github.com/buaazp/fasthttprouter" 91 | "github.com/valyala/fasthttp" 92 | ) 93 | 94 | func Index(ctx *fasthttp.RequestCtx) { 95 | fmt.Fprint(ctx, "Welcome!\n") 96 | } 97 | 98 | func Hello(ctx *fasthttp.RequestCtx) { 99 | fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 100 | } 101 | 102 | func main() { 103 | router := fasthttprouter.New() 104 | router.GET("/", Index) 105 | router.GET("/hello/:name", Hello) 106 | 107 | log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 108 | } 109 | ``` 110 | 111 | ### Named parameters 112 | 113 | As you can see, `:name` is a *named parameter*. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`. 114 | 115 | Named parameters only match a single path segment: 116 | 117 | ``` 118 | Pattern: /user/:user 119 | 120 | /user/gordon match 121 | /user/you match 122 | /user/gordon/profile no match 123 | /user/ no match 124 | ``` 125 | 126 | **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. 127 | 128 | ### Catch-All parameters 129 | 130 | The second type are *catch-all* parameters and have the form `*name`. 131 | Like the name suggests, they match everything. 132 | Therefore they must always be at the **end** of the pattern: 133 | 134 | ``` 135 | Pattern: /src/*filepath 136 | 137 | /src/ match 138 | /src/somefile.go match 139 | /src/subdir/somefile.go match 140 | ``` 141 | 142 | ## How does it work? 143 | 144 | 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: 145 | 146 | ``` 147 | Priority Path Handle 148 | 9 \ *<1> 149 | 3 ├s nil 150 | 2 |├earch\ *<2> 151 | 1 |└upport\ *<3> 152 | 2 ├blog\ *<4> 153 | 1 | └:post nil 154 | 1 | └\ *<5> 155 | 2 ├about-us\ *<6> 156 | 1 | └team\ *<7> 157 | 1 └contact\ *<8> 158 | ``` 159 | 160 | 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][benchmark], this works very well and efficient. 161 | 162 | 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, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree. 163 | 164 | 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: 165 | 166 | 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. 167 | 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. 168 | 169 | ``` 170 | ├------------ 171 | ├--------- 172 | ├----- 173 | ├---- 174 | ├-- 175 | ├-- 176 | └- 177 | ``` 178 | 179 | ## Why doesn't this work with `http.Handler`? 180 | 181 | Because fasthttp doesn't provide http.Handler. See this [description](https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp). 182 | 183 | Fasthttp works with [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler) functions instead of objects implementing Handler interface. So a FastHttpRouter provides a [Handler](https://godoc.org/github.com/buaazp/fasthttprouter#Router.Handler) interface to implement the fasthttp.ListenAndServe interface. 184 | 185 | Just try it out for yourself, the usage of FastHttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. 186 | 187 | ## Where can I find Middleware *X*? 188 | 189 | This package just provides a very efficient request router with a few extra features. The router is just a [`fasthttp.RequestHandler`](https://godoc.org/github.com/valyala/fasthttp#RequestHandler), you can chain any `fasthttp.RequestHandler` compatible middleware before the router. Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy! 190 | 191 | Have a look at these middleware examples: 192 | 193 | - [Auth Middleware](examples/auth) 194 | - [Multi Hosts Middleware](examples/hosts) 195 | 196 | ## Chaining with the NotFound handler 197 | 198 | **NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** 199 | 200 | You can use another [http.Handler](http://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](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) handler. This allows chaining. 201 | 202 | ### Static files 203 | 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): 204 | 205 | ```go 206 | // Serve static files from the ./public directory 207 | router.NotFound = fasthttp.FSHandler("./public", 0) 208 | ``` 209 | 210 | 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`. 211 | 212 | ## Web Frameworks based on FastHttpRouter 213 | 214 | 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: 215 | 216 | - Waiting for you to do this... 217 | -------------------------------------------------------------------------------- /examples/auth/README.md: -------------------------------------------------------------------------------- 1 | # Example of FastHttpRouter 2 | 3 | These examples show you the usage of `fasthttprouter`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. 4 | 5 | ### Basic Authentication 6 | 7 | Basic Authentication (RFC 2617) for handles: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "encoding/base64" 14 | "fmt" 15 | "log" 16 | "strings" 17 | 18 | "github.com/buaazp/fasthttprouter" 19 | "github.com/valyala/fasthttp" 20 | ) 21 | 22 | // basicAuth returns the username and password provided in the request's 23 | // Authorization header, if the request uses HTTP Basic Authentication. 24 | // See RFC 2617, Section 2. 25 | func basicAuth(ctx *fasthttp.RequestCtx) (username, password string, ok bool) { 26 | auth := ctx.Request.Header.Peek("Authorization") 27 | if auth == nil { 28 | return 29 | } 30 | return parseBasicAuth(string(auth)) 31 | } 32 | 33 | // parseBasicAuth parses an HTTP Basic Authentication string. 34 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). 35 | func parseBasicAuth(auth string) (username, password string, ok bool) { 36 | const prefix = "Basic " 37 | if !strings.HasPrefix(auth, prefix) { 38 | return 39 | } 40 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) 41 | if err != nil { 42 | return 43 | } 44 | cs := string(c) 45 | s := strings.IndexByte(cs, ':') 46 | if s < 0 { 47 | return 48 | } 49 | return cs[:s], cs[s+1:], true 50 | } 51 | 52 | // BasicAuth is the basic auth handler 53 | func BasicAuth(h fasthttp.RequestHandler, requiredUser, requiredPassword string) fasthttp.RequestHandler { 54 | return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { 55 | // Get the Basic Authentication credentials 56 | user, password, hasAuth := basicAuth(ctx) 57 | 58 | if hasAuth && user == requiredUser && password == requiredPassword { 59 | // Delegate request to the given handle 60 | h(ctx) 61 | return 62 | } 63 | // Request Basic Authentication otherwise 64 | ctx.Error(fasthttp.StatusMessage(fasthttp.StatusUnauthorized), fasthttp.StatusUnauthorized) 65 | ctx.Response.Header.Set("WWW-Authenticate", "Basic realm=Restricted") 66 | }) 67 | } 68 | 69 | // Index is the index handler 70 | func Index(ctx *fasthttp.RequestCtx) { 71 | fmt.Fprint(ctx, "Not protected!\n") 72 | } 73 | 74 | // Protected is the Protected handler 75 | func Protected(ctx *fasthttp.RequestCtx) { 76 | fmt.Fprint(ctx, "Protected!\n") 77 | } 78 | 79 | func main() { 80 | user := "gordon" 81 | pass := "secret!" 82 | 83 | router := fasthttprouter.New() 84 | router.GET("/", Index) 85 | router.GET("/protected/", BasicAuth(Protected, user, pass)) 86 | 87 | log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /examples/auth/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/buaazp/fasthttprouter" 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | // basicAuth returns the username and password provided in the request's 14 | // Authorization header, if the request uses HTTP Basic Authentication. 15 | // See RFC 2617, Section 2. 16 | func basicAuth(ctx *fasthttp.RequestCtx) (username, password string, ok bool) { 17 | auth := ctx.Request.Header.Peek("Authorization") 18 | if auth == nil { 19 | return 20 | } 21 | return parseBasicAuth(string(auth)) 22 | } 23 | 24 | // parseBasicAuth parses an HTTP Basic Authentication string. 25 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). 26 | func parseBasicAuth(auth string) (username, password string, ok bool) { 27 | const prefix = "Basic " 28 | if !strings.HasPrefix(auth, prefix) { 29 | return 30 | } 31 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) 32 | if err != nil { 33 | return 34 | } 35 | cs := string(c) 36 | s := strings.IndexByte(cs, ':') 37 | if s < 0 { 38 | return 39 | } 40 | return cs[:s], cs[s+1:], true 41 | } 42 | 43 | // BasicAuth is the basic auth handler 44 | func BasicAuth(h fasthttp.RequestHandler, requiredUser, requiredPassword string) fasthttp.RequestHandler { 45 | return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { 46 | // Get the Basic Authentication credentials 47 | user, password, hasAuth := basicAuth(ctx) 48 | 49 | if hasAuth && user == requiredUser && password == requiredPassword { 50 | // Delegate request to the given handle 51 | h(ctx) 52 | return 53 | } 54 | // Request Basic Authentication otherwise 55 | ctx.Error(fasthttp.StatusMessage(fasthttp.StatusUnauthorized), fasthttp.StatusUnauthorized) 56 | ctx.Response.Header.Set("WWW-Authenticate", "Basic realm=Restricted") 57 | }) 58 | } 59 | 60 | // Index is the index handler 61 | func Index(ctx *fasthttp.RequestCtx) { 62 | fmt.Fprint(ctx, "Not protected!\n") 63 | } 64 | 65 | // Protected is the Protected handler 66 | func Protected(ctx *fasthttp.RequestCtx) { 67 | fmt.Fprint(ctx, "Protected!\n") 68 | } 69 | 70 | func main() { 71 | user := "gordon" 72 | pass := "secret!" 73 | 74 | router := fasthttprouter.New() 75 | router.GET("/", Index) 76 | router.GET("/protected/", BasicAuth(Protected, user, pass)) 77 | 78 | log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 79 | } 80 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Example of FastHttpRouter 2 | 3 | These examples show you the usage of `fasthttprouter`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. 4 | 5 | ### Basic example 6 | 7 | This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter) for details. 8 | 9 | Let's start with a trivial example: 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "log" 17 | 18 | "github.com/buaazp/fasthttprouter" 19 | "github.com/valyala/fasthttp" 20 | ) 21 | 22 | // Index is the index handler 23 | func Index(ctx *fasthttp.RequestCtx) { 24 | fmt.Fprint(ctx, "Welcome!\n") 25 | } 26 | 27 | // Hello is the Hello handler 28 | func Hello(ctx *fasthttp.RequestCtx) { 29 | fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 30 | } 31 | 32 | // MultiParams is the multi params handler 33 | func MultiParams(ctx *fasthttp.RequestCtx) { 34 | fmt.Fprintf(ctx, "hi, %s, %s!\n", ctx.UserValue("name"), ctx.UserValue("word")) 35 | } 36 | 37 | // QueryArgs is used for uri query args test #11: 38 | // if the req uri is /ping?name=foo, output: Pong! foo 39 | // if the req uri is /piNg?name=foo, redirect to /ping, output: Pong! 40 | func QueryArgs(ctx *fasthttp.RequestCtx) { 41 | name := ctx.QueryArgs().Peek("name") 42 | fmt.Fprintf(ctx, "Pong! %s\n", string(name)) 43 | } 44 | 45 | func main() { 46 | router := fasthttprouter.New() 47 | router.GET("/", Index) 48 | router.GET("/hello/:name", Hello) 49 | router.GET("/multi/:name/:word", MultiParams) 50 | router.GET("/ping", QueryArgs) 51 | 52 | log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /examples/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/buaazp/fasthttprouter" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | // Index is the index handler 12 | func Index(ctx *fasthttp.RequestCtx) { 13 | fmt.Fprint(ctx, "Welcome!\n") 14 | } 15 | 16 | // Hello is the Hello handler 17 | func Hello(ctx *fasthttp.RequestCtx) { 18 | fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 19 | } 20 | 21 | // MultiParams is the multi params handler 22 | func MultiParams(ctx *fasthttp.RequestCtx) { 23 | fmt.Fprintf(ctx, "hi, %s, %s!\n", ctx.UserValue("name"), ctx.UserValue("word")) 24 | } 25 | 26 | // QueryArgs is used for uri query args test #11: 27 | // if the req uri is /ping?name=foo, output: Pong! foo 28 | // if the req uri is /piNg?name=foo, redirect to /ping, output: Pong! 29 | func QueryArgs(ctx *fasthttp.RequestCtx) { 30 | name := ctx.QueryArgs().Peek("name") 31 | fmt.Fprintf(ctx, "Pong! %s\n", string(name)) 32 | } 33 | 34 | func main() { 35 | router := fasthttprouter.New() 36 | router.GET("/", Index) 37 | router.GET("/hello/:name", Hello) 38 | router.GET("/multi/:name/:word", MultiParams) 39 | router.GET("/ping", QueryArgs) 40 | 41 | log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 42 | } 43 | -------------------------------------------------------------------------------- /examples/hosts/README.md: -------------------------------------------------------------------------------- 1 | # Example of FastHttpRouter 2 | 3 | These examples show you the usage of `fasthttprouter`. You can easily build a web application with it. Or you can make your own midwares such as custom logger, metrics, or any one you want. 4 | 5 | ### Multi-domain / Sub-domains 6 | 7 | Here is a quick example: Does your server serve multiple domains / hosts? 8 | You want to use sub-domains? 9 | Define a router per host! 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "log" 17 | 18 | "github.com/buaazp/fasthttprouter" 19 | "github.com/valyala/fasthttp" 20 | ) 21 | 22 | // Index is the index handler 23 | func Index(ctx *fasthttp.RequestCtx) { 24 | fmt.Fprint(ctx, "Welcome!\n") 25 | } 26 | 27 | // Hello is the Hello handler 28 | func Hello(ctx *fasthttp.RequestCtx) { 29 | fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 30 | } 31 | 32 | // HostSwitch is the host-handler map 33 | // We need an object that implements the fasthttp.RequestHandler interface. 34 | // We just use a map here, in which we map host names (with port) to fasthttp.RequestHandlers 35 | type HostSwitch map[string]fasthttp.RequestHandler 36 | 37 | // CheckHost Implement a CheckHost method on our new type 38 | func (hs HostSwitch) CheckHost(ctx *fasthttp.RequestCtx) { 39 | // Check if a http.Handler is registered for the given host. 40 | // If yes, use it to handle the request. 41 | if handler := hs[string(ctx.Host())]; handler != nil { 42 | handler(ctx) 43 | } else { 44 | // Handle host names for wich no handler is registered 45 | ctx.Error("Forbidden", 403) // Or Redirect? 46 | } 47 | } 48 | 49 | func main() { 50 | // Initialize a router as usual 51 | router := fasthttprouter.New() 52 | router.GET("/", Index) 53 | router.GET("/hello/:name", Hello) 54 | 55 | // Make a new HostSwitch and insert the router (our http handler) 56 | // for example.com and port 12345 57 | hs := make(HostSwitch) 58 | hs["example.com:12345"] = router.Handler 59 | 60 | // Use the HostSwitch to listen and serve on port 12345 61 | log.Fatal(fasthttp.ListenAndServe(":12345", hs.CheckHost)) 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /examples/hosts/hosts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/buaazp/fasthttprouter" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | // Index is the index handler 12 | func Index(ctx *fasthttp.RequestCtx) { 13 | fmt.Fprint(ctx, "Welcome!\n") 14 | } 15 | 16 | // Hello is the Hello handler 17 | func Hello(ctx *fasthttp.RequestCtx) { 18 | fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 19 | } 20 | 21 | // HostSwitch is the host-handler map 22 | // We need an object that implements the fasthttp.RequestHandler interface. 23 | // We just use a map here, in which we map host names (with port) to fasthttp.RequestHandlers 24 | type HostSwitch map[string]fasthttp.RequestHandler 25 | 26 | // CheckHost Implement a CheckHost method on our new type 27 | func (hs HostSwitch) CheckHost(ctx *fasthttp.RequestCtx) { 28 | // Check if a http.Handler is registered for the given host. 29 | // If yes, use it to handle the request. 30 | if handler := hs[string(ctx.Host())]; handler != nil { 31 | handler(ctx) 32 | } else { 33 | // Handle host names for which no handler is registered 34 | ctx.Error("Forbidden", 403) // Or Redirect? 35 | } 36 | } 37 | 38 | func main() { 39 | // Initialize a router as usual 40 | router := fasthttprouter.New() 41 | router.GET("/", Index) 42 | router.GET("/hello/:name", Hello) 43 | 44 | // Make a new HostSwitch and insert the router (our http handler) 45 | // for example.com and port 12345 46 | hs := make(HostSwitch) 47 | hs["example.com:12345"] = router.Handler 48 | 49 | // Use the HostSwitch to listen and serve on port 12345 50 | log.Fatal(fasthttp.ListenAndServe(":12345", hs.CheckHost)) 51 | } 52 | -------------------------------------------------------------------------------- /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 fasthttprouter 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 | // Turn empty string into "/" 23 | if p == "" { 24 | return "/" 25 | } 26 | 27 | n := len(p) 28 | var buf []byte 29 | 30 | // Invariants: 31 | // reading from path; r is index of next byte to process. 32 | // writing to buf; w is index of next byte to write. 33 | 34 | // path must start with '/' 35 | r := 1 36 | w := 1 37 | 38 | if p[0] != '/' { 39 | r = 0 40 | buf = make([]byte, n+1) 41 | buf[0] = '/' 42 | } 43 | 44 | trailing := n > 2 && p[n-1] == '/' 45 | 46 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 47 | // gets completely inlined (bufApp). So in contrast to the path package this 48 | // loop has no expensive function calls (except 1x make) 49 | 50 | for r < n { 51 | switch { 52 | case p[r] == '/': 53 | // empty path element, trailing slash is added after the end 54 | r++ 55 | 56 | case p[r] == '.' && r+1 == n: 57 | trailing = true 58 | r++ 59 | 60 | case p[r] == '.' && p[r+1] == '/': 61 | // . element 62 | r++ 63 | 64 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 65 | // .. element: remove to last / 66 | r += 2 67 | 68 | if w > 1 { 69 | // can backtrack 70 | w-- 71 | 72 | if buf == nil { 73 | for w > 1 && p[w] != '/' { 74 | w-- 75 | } 76 | } else { 77 | for w > 1 && buf[w] != '/' { 78 | w-- 79 | } 80 | } 81 | } 82 | 83 | default: 84 | // real path element. 85 | // add slash if needed 86 | if w > 1 { 87 | bufApp(&buf, p, w, '/') 88 | w++ 89 | } 90 | 91 | // copy element 92 | for r < n && p[r] != '/' { 93 | bufApp(&buf, p, w, p[r]) 94 | w++ 95 | r++ 96 | } 97 | } 98 | } 99 | 100 | // re-append trailing slash 101 | if trailing && w > 1 { 102 | bufApp(&buf, p, w, '/') 103 | w++ 104 | } 105 | 106 | if buf == nil { 107 | return p[:w] 108 | } 109 | return string(buf[:w]) 110 | } 111 | 112 | // internal helper to lazily create a buffer if necessary 113 | func bufApp(buf *[]byte, s string, w int, c byte) { 114 | if *buf == nil { 115 | if s[w] == c { 116 | return 117 | } 118 | 119 | *buf = make([]byte, len(s)) 120 | copy(*buf, s[:w]) 121 | } 122 | (*buf)[w] = c 123 | } 124 | -------------------------------------------------------------------------------- /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 fasthttprouter 7 | 8 | import ( 9 | "runtime" 10 | "testing" 11 | ) 12 | 13 | var cleanTests = []struct { 14 | path, result string 15 | }{ 16 | // Already clean 17 | {"/", "/"}, 18 | {"/abc", "/abc"}, 19 | {"/a/b/c", "/a/b/c"}, 20 | {"/abc/", "/abc/"}, 21 | {"/a/b/c/", "/a/b/c/"}, 22 | 23 | // missing root 24 | {"", "/"}, 25 | {"abc", "/abc"}, 26 | {"abc/def", "/abc/def"}, 27 | {"a/b/c", "/a/b/c"}, 28 | 29 | // Remove doubled slash 30 | {"//", "/"}, 31 | {"/abc//", "/abc/"}, 32 | {"/abc/def//", "/abc/def/"}, 33 | {"/a/b/c//", "/a/b/c/"}, 34 | {"/abc//def//ghi", "/abc/def/ghi"}, 35 | {"//abc", "/abc"}, 36 | {"///abc", "/abc"}, 37 | {"//abc//", "/abc/"}, 38 | 39 | // Remove . elements 40 | {".", "/"}, 41 | {"./", "/"}, 42 | {"/abc/./def", "/abc/def"}, 43 | {"/./abc/def", "/abc/def"}, 44 | {"/abc/.", "/abc/"}, 45 | 46 | // Remove .. elements 47 | {"..", "/"}, 48 | {"../", "/"}, 49 | {"../../", "/"}, 50 | {"../..", "/"}, 51 | {"../../abc", "/abc"}, 52 | {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, 53 | {"/abc/def/../ghi/../jkl", "/abc/jkl"}, 54 | {"/abc/def/..", "/abc"}, 55 | {"/abc/def/../..", "/"}, 56 | {"/abc/def/../../..", "/"}, 57 | {"/abc/def/../../..", "/"}, 58 | {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, 59 | 60 | // Combinations 61 | {"abc/./../def", "/def"}, 62 | {"abc//./../def", "/def"}, 63 | {"abc/../../././../def", "/def"}, 64 | } 65 | 66 | func TestPathClean(t *testing.T) { 67 | for _, test := range cleanTests { 68 | if s := CleanPath(test.path); s != test.result { 69 | t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) 70 | } 71 | if s := CleanPath(test.result); s != test.result { 72 | t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) 73 | } 74 | } 75 | } 76 | 77 | func TestPathCleanMallocs(t *testing.T) { 78 | if testing.Short() { 79 | t.Skip("skipping malloc count in short mode") 80 | } 81 | if runtime.GOMAXPROCS(0) > 1 { 82 | t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") 83 | return 84 | } 85 | 86 | for _, test := range cleanTests { 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 | -------------------------------------------------------------------------------- /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 fasthttprouter 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 | // "log" 14 | // 15 | // "github.com/buaazp/fasthttprouter" 16 | // "github.com/valyala/fasthttp" 17 | // ) 18 | 19 | // func Index(ctx *fasthttp.RequestCtx) { 20 | // fmt.Fprint(ctx, "Welcome!\n") 21 | // } 22 | 23 | // func Hello(ctx *fasthttp.RequestCtx) { 24 | // fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name")) 25 | // } 26 | 27 | // func main() { 28 | // router := fasthttprouter.New() 29 | // router.GET("/", Index) 30 | // router.GET("/hello/:name", Hello) 31 | 32 | // log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler)) 33 | // } 34 | // 35 | // The router matches incoming requests by the request method and the path. 36 | // If a handle is registered for this path and method, the router delegates the 37 | // request to that function. 38 | // For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to 39 | // register handles, for all other methods router.Handle can be used. 40 | // 41 | // The registered path, against which the router matches incoming requests, can 42 | // contain two types of parameters: 43 | // Syntax Type 44 | // :name named parameter 45 | // *name catch-all parameter 46 | // 47 | // Named parameters are dynamic path segments. They match anything until the 48 | // next '/' or the path end: 49 | // Path: /blog/:category/:post 50 | // 51 | // Requests: 52 | // /blog/go/request-routers match: category="go", post="request-routers" 53 | // /blog/go/request-routers/ no match, but the router would redirect 54 | // /blog/go/ no match 55 | // /blog/go/request-routers/comments no match 56 | // 57 | // Catch-all parameters match anything until the path end, including the 58 | // directory index (the '/' before the catch-all). Since they match anything 59 | // until the end, catch-all parameters must always be the final path element. 60 | // Path: /files/*filepath 61 | // 62 | // Requests: 63 | // /files/ match: filepath="/" 64 | // /files/LICENSE match: filepath="/LICENSE" 65 | // /files/templates/article.html match: filepath="/templates/article.html" 66 | // /files no match, but the router would redirect 67 | // 68 | // The value of parameters is inside ctx.UserValue 69 | // To retrieve the value of a parameter: 70 | // // use the name of the parameter 71 | // user := ps.UserValue("user") 72 | // 73 | 74 | package fasthttprouter 75 | 76 | import ( 77 | "strings" 78 | 79 | "github.com/valyala/fasthttp" 80 | ) 81 | 82 | var ( 83 | defaultContentType = []byte("text/plain; charset=utf-8") 84 | questionMark = []byte("?") 85 | ) 86 | 87 | // Router is a http.Handler which can be used to dispatch requests to different 88 | // handler functions via configurable routes 89 | type Router struct { 90 | trees map[string]*node 91 | 92 | // Enables automatic redirection if the current route can't be matched but a 93 | // handler for the path with (without) the trailing slash exists. 94 | // For example if /foo/ is requested but a route only exists for /foo, the 95 | // client is redirected to /foo with http status code 301 for GET requests 96 | // and 307 for all other request methods. 97 | RedirectTrailingSlash bool 98 | 99 | // If enabled, the router tries to fix the current request path, if no 100 | // handle is registered for it. 101 | // First superfluous path elements like ../ or // are removed. 102 | // Afterwards the router does a case-insensitive lookup of the cleaned path. 103 | // If a handle can be found for this route, the router makes a redirection 104 | // to the corrected path with status code 301 for GET requests and 307 for 105 | // all other request methods. 106 | // For example /FOO and /..//Foo could be redirected to /foo. 107 | // RedirectTrailingSlash is independent of this option. 108 | RedirectFixedPath bool 109 | 110 | // If enabled, the router checks if another method is allowed for the 111 | // current route, if the current request can not be routed. 112 | // If this is the case, the request is answered with 'Method Not Allowed' 113 | // and HTTP status code 405. 114 | // If no other Method is allowed, the request is delegated to the NotFound 115 | // handler. 116 | HandleMethodNotAllowed bool 117 | 118 | // If enabled, the router automatically replies to OPTIONS requests. 119 | // Custom OPTIONS handlers take priority over automatic replies. 120 | HandleOPTIONS bool 121 | 122 | // Configurable http.Handler which is called when no matching route is 123 | // found. If it is not set, http.NotFound is used. 124 | NotFound fasthttp.RequestHandler 125 | 126 | // Configurable http.Handler which is called when a request 127 | // cannot be routed and HandleMethodNotAllowed is true. 128 | // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 129 | // The "Allow" header with allowed request methods is set before the handler 130 | // is called. 131 | MethodNotAllowed fasthttp.RequestHandler 132 | 133 | // Function to handle panics recovered from http handlers. 134 | // It should be used to generate a error page and return the http error code 135 | // 500 (Internal Server Error). 136 | // The handler can be used to keep your server from crashing because of 137 | // unrecovered panics. 138 | PanicHandler func(*fasthttp.RequestCtx, interface{}) 139 | } 140 | 141 | // New returns a new initialized Router. 142 | // Path auto-correction, including trailing slashes, is enabled by default. 143 | func New() *Router { 144 | return &Router{ 145 | RedirectTrailingSlash: true, 146 | RedirectFixedPath: true, 147 | HandleMethodNotAllowed: true, 148 | HandleOPTIONS: true, 149 | } 150 | } 151 | 152 | // GET is a shortcut for router.Handle("GET", path, handle) 153 | func (r *Router) GET(path string, handle fasthttp.RequestHandler) { 154 | r.Handle("GET", path, handle) 155 | } 156 | 157 | // HEAD is a shortcut for router.Handle("HEAD", path, handle) 158 | func (r *Router) HEAD(path string, handle fasthttp.RequestHandler) { 159 | r.Handle("HEAD", path, handle) 160 | } 161 | 162 | // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) 163 | func (r *Router) OPTIONS(path string, handle fasthttp.RequestHandler) { 164 | r.Handle("OPTIONS", path, handle) 165 | } 166 | 167 | // POST is a shortcut for router.Handle("POST", path, handle) 168 | func (r *Router) POST(path string, handle fasthttp.RequestHandler) { 169 | r.Handle("POST", path, handle) 170 | } 171 | 172 | // PUT is a shortcut for router.Handle("PUT", path, handle) 173 | func (r *Router) PUT(path string, handle fasthttp.RequestHandler) { 174 | r.Handle("PUT", path, handle) 175 | } 176 | 177 | // PATCH is a shortcut for router.Handle("PATCH", path, handle) 178 | func (r *Router) PATCH(path string, handle fasthttp.RequestHandler) { 179 | r.Handle("PATCH", path, handle) 180 | } 181 | 182 | // DELETE is a shortcut for router.Handle("DELETE", path, handle) 183 | func (r *Router) DELETE(path string, handle fasthttp.RequestHandler) { 184 | r.Handle("DELETE", path, handle) 185 | } 186 | 187 | // Handle registers a new request handle with the given path and method. 188 | // 189 | // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 190 | // functions can be used. 191 | // 192 | // This function is intended for bulk loading and to allow the usage of less 193 | // frequently used, non-standardized or custom methods (e.g. for internal 194 | // communication with a proxy). 195 | func (r *Router) Handle(method, path string, handle fasthttp.RequestHandler) { 196 | if path[0] != '/' { 197 | panic("path must begin with '/' in path '" + path + "'") 198 | } 199 | 200 | if r.trees == nil { 201 | r.trees = make(map[string]*node) 202 | } 203 | 204 | root := r.trees[method] 205 | if root == nil { 206 | root = new(node) 207 | r.trees[method] = root 208 | } 209 | 210 | root.addRoute(path, handle) 211 | } 212 | 213 | // ServeFiles serves files from the given file system root. 214 | // The path must end with "/*filepath", files are then served from the local 215 | // path /defined/root/dir/*filepath. 216 | // For example if root is "/etc" and *filepath is "passwd", the local file 217 | // "/etc/passwd" would be served. 218 | // Internally a http.FileServer is used, therefore http.NotFound is used instead 219 | // of the Router's NotFound handler. 220 | // router.ServeFiles("/src/*filepath", "/var/www") 221 | func (r *Router) ServeFiles(path string, rootPath string) { 222 | if len(path) < 10 || path[len(path)-10:] != "/*filepath" { 223 | panic("path must end with /*filepath in path '" + path + "'") 224 | } 225 | prefix := path[:len(path)-10] 226 | 227 | fileHandler := fasthttp.FSHandler(rootPath, strings.Count(prefix, "/")) 228 | 229 | r.GET(path, func(ctx *fasthttp.RequestCtx) { 230 | fileHandler(ctx) 231 | }) 232 | } 233 | 234 | func (r *Router) recv(ctx *fasthttp.RequestCtx) { 235 | if rcv := recover(); rcv != nil { 236 | r.PanicHandler(ctx, rcv) 237 | } 238 | } 239 | 240 | // Lookup allows the manual lookup of a method + path combo. 241 | // This is e.g. useful to build a framework around this router. 242 | // If the path was found, it returns the handle function and the path parameter 243 | // values. Otherwise the third return value indicates whether a redirection to 244 | // the same path with an extra / without the trailing slash should be performed. 245 | func (r *Router) Lookup(method, path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) { 246 | if root := r.trees[method]; root != nil { 247 | return root.getValue(path, ctx) 248 | } 249 | return nil, false 250 | } 251 | 252 | func (r *Router) allowed(path, reqMethod string) (allow string) { 253 | if path == "*" || path == "/*" { // server-wide 254 | for method := range r.trees { 255 | if method == "OPTIONS" { 256 | continue 257 | } 258 | 259 | // add request method to list of allowed methods 260 | if len(allow) == 0 { 261 | allow = method 262 | } else { 263 | allow += ", " + method 264 | } 265 | } 266 | } else { // specific path 267 | for method := range r.trees { 268 | // Skip the requested method - we already tried this one 269 | if method == reqMethod || method == "OPTIONS" { 270 | continue 271 | } 272 | 273 | handle, _ := r.trees[method].getValue(path, nil) 274 | if handle != nil { 275 | // add request method to list of allowed methods 276 | if len(allow) == 0 { 277 | allow = method 278 | } else { 279 | allow += ", " + method 280 | } 281 | } 282 | } 283 | } 284 | if len(allow) > 0 { 285 | allow += ", OPTIONS" 286 | } 287 | return 288 | } 289 | 290 | // Handler makes the router implement the fasthttp.ListenAndServe interface. 291 | func (r *Router) Handler(ctx *fasthttp.RequestCtx) { 292 | if r.PanicHandler != nil { 293 | defer r.recv(ctx) 294 | } 295 | 296 | path := string(ctx.URI().PathOriginal()) 297 | method := string(ctx.Method()) 298 | if root := r.trees[method]; root != nil { 299 | if f, tsr := root.getValue(path, ctx); f != nil { 300 | f(ctx) 301 | return 302 | } else if method != "CONNECT" && path != "/" { 303 | code := 301 // Permanent redirect, request with GET method 304 | if method != "GET" { 305 | // Temporary redirect, request with same method 306 | // As of Go 1.3, Go does not support status code 308. 307 | code = 307 308 | } 309 | 310 | if tsr && r.RedirectTrailingSlash { 311 | var uri string 312 | if len(path) > 1 && path[len(path)-1] == '/' { 313 | uri = path[:len(path)-1] 314 | } else { 315 | uri = path + "/" 316 | } 317 | 318 | if len(ctx.URI().QueryString()) > 0 { 319 | uri += "?" + string(ctx.QueryArgs().QueryString()) 320 | } 321 | 322 | ctx.Redirect(uri, code) 323 | return 324 | } 325 | 326 | // Try to fix the request path 327 | if r.RedirectFixedPath { 328 | fixedPath, found := root.findCaseInsensitivePath( 329 | CleanPath(path), 330 | r.RedirectTrailingSlash, 331 | ) 332 | 333 | if found { 334 | queryBuf := ctx.URI().QueryString() 335 | if len(queryBuf) > 0 { 336 | fixedPath = append(fixedPath, questionMark...) 337 | fixedPath = append(fixedPath, queryBuf...) 338 | } 339 | uri := string(fixedPath) 340 | ctx.Redirect(uri, code) 341 | return 342 | } 343 | } 344 | } 345 | } 346 | 347 | if method == "OPTIONS" { 348 | // Handle OPTIONS requests 349 | if r.HandleOPTIONS { 350 | if allow := r.allowed(path, method); len(allow) > 0 { 351 | ctx.Response.Header.Set("Allow", allow) 352 | return 353 | } 354 | } 355 | } else { 356 | // Handle 405 357 | if r.HandleMethodNotAllowed { 358 | if allow := r.allowed(path, method); len(allow) > 0 { 359 | ctx.Response.Header.Set("Allow", allow) 360 | if r.MethodNotAllowed != nil { 361 | r.MethodNotAllowed(ctx) 362 | } else { 363 | ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed) 364 | ctx.SetContentTypeBytes(defaultContentType) 365 | ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed)) 366 | } 367 | return 368 | } 369 | } 370 | } 371 | 372 | // Handle 404 373 | if r.NotFound != nil { 374 | r.NotFound(ctx) 375 | } else { 376 | ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound), 377 | fasthttp.StatusNotFound) 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /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 fasthttprouter 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "net" 14 | "net/http" 15 | "os" 16 | "testing" 17 | "time" 18 | 19 | "github.com/valyala/fasthttp" 20 | ) 21 | 22 | func TestRouter(t *testing.T) { 23 | router := New() 24 | 25 | routed := false 26 | router.Handle("GET", "/user/:name", func(ctx *fasthttp.RequestCtx) { 27 | routed = true 28 | want := map[string]string{"name": "gopher"} 29 | 30 | if ctx.UserValue("name") != want["name"] { 31 | t.Fatalf("wrong wildcard values: want %v, got %v", want["name"], ctx.UserValue("name")) 32 | } 33 | ctx.Success("foo/bar", []byte("success")) 34 | }) 35 | 36 | s := &fasthttp.Server{ 37 | Handler: router.Handler, 38 | } 39 | 40 | rw := &readWriter{} 41 | rw.r.WriteString("GET /user/gopher?baz HTTP/1.1\r\n\r\n") 42 | 43 | ch := make(chan error) 44 | go func() { 45 | ch <- s.ServeConn(rw) 46 | }() 47 | 48 | select { 49 | case err := <-ch: 50 | if err != nil { 51 | t.Fatalf("return error %s", err) 52 | } 53 | case <-time.After(100 * time.Millisecond): 54 | t.Fatalf("timeout") 55 | } 56 | 57 | if !routed { 58 | t.Fatal("routing failed") 59 | } 60 | } 61 | 62 | type handlerStruct struct { 63 | handeled *bool 64 | } 65 | 66 | func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { 67 | *h.handeled = true 68 | } 69 | 70 | func TestRouterAPI(t *testing.T) { 71 | var get, head, options, post, put, patch, deleted bool 72 | 73 | router := New() 74 | router.GET("/GET", func(ctx *fasthttp.RequestCtx) { 75 | get = true 76 | }) 77 | router.HEAD("/GET", func(ctx *fasthttp.RequestCtx) { 78 | head = true 79 | }) 80 | router.OPTIONS("/GET", func(ctx *fasthttp.RequestCtx) { 81 | options = true 82 | }) 83 | router.POST("/POST", func(ctx *fasthttp.RequestCtx) { 84 | post = true 85 | }) 86 | router.PUT("/PUT", func(ctx *fasthttp.RequestCtx) { 87 | put = true 88 | }) 89 | router.PATCH("/PATCH", func(ctx *fasthttp.RequestCtx) { 90 | patch = true 91 | }) 92 | router.DELETE("/DELETE", func(ctx *fasthttp.RequestCtx) { 93 | deleted = true 94 | }) 95 | 96 | s := &fasthttp.Server{ 97 | Handler: router.Handler, 98 | } 99 | 100 | rw := &readWriter{} 101 | ch := make(chan error) 102 | 103 | rw.r.WriteString("GET /GET HTTP/1.1\r\n\r\n") 104 | go func() { 105 | ch <- s.ServeConn(rw) 106 | }() 107 | select { 108 | case err := <-ch: 109 | if err != nil { 110 | t.Fatalf("return error %s", err) 111 | } 112 | case <-time.After(100 * time.Millisecond): 113 | t.Fatalf("timeout") 114 | } 115 | if !get { 116 | t.Error("routing GET failed") 117 | } 118 | 119 | rw.r.WriteString("HEAD /GET HTTP/1.1\r\n\r\n") 120 | go func() { 121 | ch <- s.ServeConn(rw) 122 | }() 123 | select { 124 | case err := <-ch: 125 | if err != nil { 126 | t.Fatalf("return error %s", err) 127 | } 128 | case <-time.After(100 * time.Millisecond): 129 | t.Fatalf("timeout") 130 | } 131 | if !head { 132 | t.Error("routing HEAD failed") 133 | } 134 | 135 | rw.r.WriteString("OPTIONS /GET HTTP/1.1\r\n\r\n") 136 | go func() { 137 | ch <- s.ServeConn(rw) 138 | }() 139 | select { 140 | case err := <-ch: 141 | if err != nil { 142 | t.Fatalf("return error %s", err) 143 | } 144 | case <-time.After(100 * time.Millisecond): 145 | t.Fatalf("timeout") 146 | } 147 | if !options { 148 | t.Error("routing OPTIONS failed") 149 | } 150 | 151 | rw.r.WriteString("POST /POST HTTP/1.1\r\n\r\n") 152 | go func() { 153 | ch <- s.ServeConn(rw) 154 | }() 155 | select { 156 | case err := <-ch: 157 | if err != nil { 158 | t.Fatalf("return error %s", err) 159 | } 160 | case <-time.After(100 * time.Millisecond): 161 | t.Fatalf("timeout") 162 | } 163 | if !post { 164 | t.Error("routing POST failed") 165 | } 166 | 167 | rw.r.WriteString("PUT /PUT HTTP/1.1\r\n\r\n") 168 | go func() { 169 | ch <- s.ServeConn(rw) 170 | }() 171 | select { 172 | case err := <-ch: 173 | if err != nil { 174 | t.Fatalf("return error %s", err) 175 | } 176 | case <-time.After(100 * time.Millisecond): 177 | t.Fatalf("timeout") 178 | } 179 | if !put { 180 | t.Error("routing PUT failed") 181 | } 182 | 183 | rw.r.WriteString("PATCH /PATCH HTTP/1.1\r\n\r\n") 184 | go func() { 185 | ch <- s.ServeConn(rw) 186 | }() 187 | select { 188 | case err := <-ch: 189 | if err != nil { 190 | t.Fatalf("return error %s", err) 191 | } 192 | case <-time.After(100 * time.Millisecond): 193 | t.Fatalf("timeout") 194 | } 195 | if !patch { 196 | t.Error("routing PATCH failed") 197 | } 198 | 199 | rw.r.WriteString("DELETE /DELETE HTTP/1.1\r\n\r\n") 200 | go func() { 201 | ch <- s.ServeConn(rw) 202 | }() 203 | select { 204 | case err := <-ch: 205 | if err != nil { 206 | t.Fatalf("return error %s", err) 207 | } 208 | case <-time.After(100 * time.Millisecond): 209 | t.Fatalf("timeout") 210 | } 211 | if !deleted { 212 | t.Error("routing DELETE failed") 213 | } 214 | } 215 | 216 | func TestRouterRoot(t *testing.T) { 217 | router := New() 218 | recv := catchPanic(func() { 219 | router.GET("noSlashRoot", nil) 220 | }) 221 | if recv == nil { 222 | t.Fatal("registering path not beginning with '/' did not panic") 223 | } 224 | } 225 | 226 | func TestRouterChaining(t *testing.T) { 227 | router1 := New() 228 | router2 := New() 229 | router1.NotFound = router2.Handler 230 | 231 | fooHit := false 232 | router1.POST("/foo", func(ctx *fasthttp.RequestCtx) { 233 | fooHit = true 234 | ctx.SetStatusCode(fasthttp.StatusOK) 235 | }) 236 | 237 | barHit := false 238 | router2.POST("/bar", func(ctx *fasthttp.RequestCtx) { 239 | barHit = true 240 | ctx.SetStatusCode(fasthttp.StatusOK) 241 | }) 242 | 243 | s := &fasthttp.Server{ 244 | Handler: router1.Handler, 245 | } 246 | 247 | rw := &readWriter{} 248 | ch := make(chan error) 249 | 250 | rw.r.WriteString("POST /foo HTTP/1.1\r\n\r\n") 251 | go func() { 252 | ch <- s.ServeConn(rw) 253 | }() 254 | select { 255 | case err := <-ch: 256 | if err != nil { 257 | t.Fatalf("return error %s", err) 258 | } 259 | case <-time.After(100 * time.Millisecond): 260 | t.Fatalf("timeout") 261 | } 262 | br := bufio.NewReader(&rw.w) 263 | var resp fasthttp.Response 264 | if err := resp.Read(br); err != nil { 265 | t.Fatalf("Unexpected error when reading response: %s", err) 266 | } 267 | if !(resp.Header.StatusCode() == fasthttp.StatusOK && fooHit) { 268 | t.Errorf("Regular routing failed with router chaining.") 269 | t.FailNow() 270 | } 271 | 272 | rw.r.WriteString("POST /bar HTTP/1.1\r\n\r\n") 273 | go func() { 274 | ch <- s.ServeConn(rw) 275 | }() 276 | select { 277 | case err := <-ch: 278 | if err != nil { 279 | t.Fatalf("return error %s", err) 280 | } 281 | case <-time.After(100 * time.Millisecond): 282 | t.Fatalf("timeout") 283 | } 284 | if err := resp.Read(br); err != nil { 285 | t.Fatalf("Unexpected error when reading response: %s", err) 286 | } 287 | if !(resp.Header.StatusCode() == fasthttp.StatusOK && barHit) { 288 | t.Errorf("Chained routing failed with router chaining.") 289 | t.FailNow() 290 | } 291 | 292 | rw.r.WriteString("POST /qax HTTP/1.1\r\n\r\n") 293 | go func() { 294 | ch <- s.ServeConn(rw) 295 | }() 296 | select { 297 | case err := <-ch: 298 | if err != nil { 299 | t.Fatalf("return error %s", err) 300 | } 301 | case <-time.After(100 * time.Millisecond): 302 | t.Fatalf("timeout") 303 | } 304 | if err := resp.Read(br); err != nil { 305 | t.Fatalf("Unexpected error when reading response: %s", err) 306 | } 307 | if !(resp.Header.StatusCode() == fasthttp.StatusNotFound) { 308 | t.Errorf("NotFound behavior failed with router chaining.") 309 | t.FailNow() 310 | } 311 | } 312 | 313 | func TestRouterOPTIONS(t *testing.T) { 314 | // TODO: because fasthttp is not support OPTIONS method now, 315 | // these test cases will be used in the future. 316 | handlerFunc := func(_ *fasthttp.RequestCtx) {} 317 | 318 | router := New() 319 | router.POST("/path", handlerFunc) 320 | 321 | // test not allowed 322 | // * (server) 323 | s := &fasthttp.Server{ 324 | Handler: router.Handler, 325 | } 326 | 327 | rw := &readWriter{} 328 | ch := make(chan error) 329 | 330 | rw.r.WriteString("OPTIONS * HTTP/1.1\r\nHost:\r\n\r\n") 331 | go func() { 332 | ch <- s.ServeConn(rw) 333 | }() 334 | select { 335 | case err := <-ch: 336 | if err != nil { 337 | t.Fatalf("return error %s", err) 338 | } 339 | case <-time.After(100 * time.Millisecond): 340 | t.Fatalf("timeout") 341 | } 342 | br := bufio.NewReader(&rw.w) 343 | var resp fasthttp.Response 344 | if err := resp.Read(br); err != nil { 345 | t.Fatalf("Unexpected error when reading response: %s", err) 346 | } 347 | if resp.Header.StatusCode() != fasthttp.StatusOK { 348 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 349 | resp.Header.StatusCode(), resp.Header.String()) 350 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, OPTIONS" { 351 | t.Error("unexpected Allow header value: " + allow) 352 | } 353 | 354 | // path 355 | rw.r.WriteString("OPTIONS /path HTTP/1.1\r\n\r\n") 356 | go func() { 357 | ch <- s.ServeConn(rw) 358 | }() 359 | select { 360 | case err := <-ch: 361 | if err != nil { 362 | t.Fatalf("return error %s", err) 363 | } 364 | case <-time.After(100 * time.Millisecond): 365 | t.Fatalf("timeout") 366 | } 367 | if err := resp.Read(br); err != nil { 368 | t.Fatalf("Unexpected error when reading response: %s", err) 369 | } 370 | if resp.Header.StatusCode() != fasthttp.StatusOK { 371 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 372 | resp.Header.StatusCode(), resp.Header.String()) 373 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, OPTIONS" { 374 | t.Error("unexpected Allow header value: " + allow) 375 | } 376 | 377 | rw.r.WriteString("OPTIONS /doesnotexist HTTP/1.1\r\n\r\n") 378 | go func() { 379 | ch <- s.ServeConn(rw) 380 | }() 381 | select { 382 | case err := <-ch: 383 | if err != nil { 384 | t.Fatalf("return error %s", err) 385 | } 386 | case <-time.After(100 * time.Millisecond): 387 | t.Fatalf("timeout") 388 | } 389 | if err := resp.Read(br); err != nil { 390 | t.Fatalf("Unexpected error when reading response: %s", err) 391 | } 392 | if !(resp.Header.StatusCode() == fasthttp.StatusNotFound) { 393 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 394 | resp.Header.StatusCode(), resp.Header.String()) 395 | } 396 | 397 | // add another method 398 | router.GET("/path", handlerFunc) 399 | 400 | // test again 401 | // * (server) 402 | rw.r.WriteString("OPTIONS * HTTP/1.1\r\n\r\n") 403 | go func() { 404 | ch <- s.ServeConn(rw) 405 | }() 406 | select { 407 | case err := <-ch: 408 | if err != nil { 409 | t.Fatalf("return error %s", err) 410 | } 411 | case <-time.After(100 * time.Millisecond): 412 | t.Fatalf("timeout") 413 | } 414 | if err := resp.Read(br); err != nil { 415 | t.Fatalf("Unexpected error when reading response: %s", err) 416 | } 417 | if resp.Header.StatusCode() != fasthttp.StatusOK { 418 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 419 | resp.Header.StatusCode(), resp.Header.String()) 420 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" { 421 | t.Error("unexpected Allow header value: " + allow) 422 | } 423 | 424 | // path 425 | rw.r.WriteString("OPTIONS /path HTTP/1.1\r\n\r\n") 426 | go func() { 427 | ch <- s.ServeConn(rw) 428 | }() 429 | select { 430 | case err := <-ch: 431 | if err != nil { 432 | t.Fatalf("return error %s", err) 433 | } 434 | case <-time.After(100 * time.Millisecond): 435 | t.Fatalf("timeout") 436 | } 437 | if err := resp.Read(br); err != nil { 438 | t.Fatalf("Unexpected error when reading response: %s", err) 439 | } 440 | if resp.Header.StatusCode() != fasthttp.StatusOK { 441 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 442 | resp.Header.StatusCode(), resp.Header.String()) 443 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" { 444 | t.Error("unexpected Allow header value: " + allow) 445 | } 446 | 447 | // custom handler 448 | var custom bool 449 | router.OPTIONS("/path", func(_ *fasthttp.RequestCtx) { 450 | custom = true 451 | }) 452 | 453 | // test again 454 | // * (server) 455 | rw.r.WriteString("OPTIONS * HTTP/1.1\r\n\r\n") 456 | go func() { 457 | ch <- s.ServeConn(rw) 458 | }() 459 | select { 460 | case err := <-ch: 461 | if err != nil { 462 | t.Fatalf("return error %s", err) 463 | } 464 | case <-time.After(100 * time.Millisecond): 465 | t.Fatalf("timeout") 466 | } 467 | if err := resp.Read(br); err != nil { 468 | t.Fatalf("Unexpected error when reading response: %s", err) 469 | } 470 | if resp.Header.StatusCode() != fasthttp.StatusOK { 471 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 472 | resp.Header.StatusCode(), resp.Header.String()) 473 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, GET, OPTIONS" && allow != "GET, POST, OPTIONS" { 474 | t.Error("unexpected Allow header value: " + allow) 475 | } 476 | if custom { 477 | t.Error("custom handler called on *") 478 | } 479 | 480 | // path 481 | rw.r.WriteString("OPTIONS /path HTTP/1.1\r\n\r\n") 482 | go func() { 483 | ch <- s.ServeConn(rw) 484 | }() 485 | select { 486 | case err := <-ch: 487 | if err != nil { 488 | t.Fatalf("return error %s", err) 489 | } 490 | case <-time.After(100 * time.Millisecond): 491 | t.Fatalf("timeout") 492 | } 493 | if err := resp.Read(br); err != nil { 494 | t.Fatalf("Unexpected error when reading response: %s", err) 495 | } 496 | if resp.Header.StatusCode() != fasthttp.StatusOK { 497 | t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", 498 | resp.Header.StatusCode(), resp.Header.String()) 499 | } 500 | if !custom { 501 | t.Error("custom handler not called") 502 | } 503 | } 504 | 505 | func TestRouterNotAllowed(t *testing.T) { 506 | handlerFunc := func(_ *fasthttp.RequestCtx) {} 507 | 508 | router := New() 509 | router.POST("/path", handlerFunc) 510 | 511 | // Test not allowed 512 | s := &fasthttp.Server{ 513 | Handler: router.Handler, 514 | } 515 | 516 | rw := &readWriter{} 517 | ch := make(chan error) 518 | 519 | rw.r.WriteString("GET /path HTTP/1.1\r\n\r\n") 520 | go func() { 521 | ch <- s.ServeConn(rw) 522 | }() 523 | select { 524 | case err := <-ch: 525 | if err != nil { 526 | t.Fatalf("return error %s", err) 527 | } 528 | case <-time.After(100 * time.Millisecond): 529 | t.Fatalf("timeout") 530 | } 531 | br := bufio.NewReader(&rw.w) 532 | var resp fasthttp.Response 533 | if err := resp.Read(br); err != nil { 534 | t.Fatalf("Unexpected error when reading response: %s", err) 535 | } 536 | if !(resp.Header.StatusCode() == fasthttp.StatusMethodNotAllowed) { 537 | t.Errorf("NotAllowed handling failed: Code=%d", resp.Header.StatusCode()) 538 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, OPTIONS" { 539 | t.Error("unexpected Allow header value: " + allow) 540 | } 541 | 542 | // add another method 543 | router.DELETE("/path", handlerFunc) 544 | router.OPTIONS("/path", handlerFunc) // must be ignored 545 | 546 | // test again 547 | rw.r.WriteString("GET /path HTTP/1.1\r\n\r\n") 548 | go func() { 549 | ch <- s.ServeConn(rw) 550 | }() 551 | select { 552 | case err := <-ch: 553 | if err != nil { 554 | t.Fatalf("return error %s", err) 555 | } 556 | case <-time.After(100 * time.Millisecond): 557 | t.Fatalf("timeout") 558 | } 559 | if err := resp.Read(br); err != nil { 560 | t.Fatalf("Unexpected error when reading response: %s", err) 561 | } 562 | if !(resp.Header.StatusCode() == fasthttp.StatusMethodNotAllowed) { 563 | t.Errorf("NotAllowed handling failed: Code=%d", resp.Header.StatusCode()) 564 | } else if allow := string(resp.Header.Peek("Allow")); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" { 565 | t.Error("unexpected Allow header value: " + allow) 566 | } 567 | 568 | responseText := "custom method" 569 | router.MethodNotAllowed = fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { 570 | ctx.SetStatusCode(fasthttp.StatusTeapot) 571 | ctx.Write([]byte(responseText)) 572 | }) 573 | rw.r.WriteString("GET /path HTTP/1.1\r\n\r\n") 574 | go func() { 575 | ch <- s.ServeConn(rw) 576 | }() 577 | select { 578 | case err := <-ch: 579 | if err != nil { 580 | t.Fatalf("return error %s", err) 581 | } 582 | case <-time.After(100 * time.Millisecond): 583 | t.Fatalf("timeout") 584 | } 585 | if err := resp.Read(br); err != nil { 586 | t.Fatalf("Unexpected error when reading response: %s", err) 587 | } 588 | if !bytes.Equal(resp.Body(), []byte(responseText)) { 589 | t.Errorf("unexpected response got %q want %q", string(resp.Body()), responseText) 590 | } 591 | if resp.Header.StatusCode() != fasthttp.StatusTeapot { 592 | t.Errorf("unexpected response code %d want %d", resp.Header.StatusCode(), fasthttp.StatusTeapot) 593 | } 594 | if allow := string(resp.Header.Peek("Allow")); allow != "POST, DELETE, OPTIONS" && allow != "DELETE, POST, OPTIONS" { 595 | t.Error("unexpected Allow header value: " + allow) 596 | } 597 | } 598 | 599 | func TestRouterNotFound(t *testing.T) { 600 | handlerFunc := func(_ *fasthttp.RequestCtx) {} 601 | 602 | router := New() 603 | router.GET("/path", handlerFunc) 604 | router.GET("/dir/", handlerFunc) 605 | router.GET("/", handlerFunc) 606 | 607 | testRoutes := []struct { 608 | route string 609 | code int 610 | }{ 611 | {"/path/", 301}, // TSR -/ 612 | {"/dir", 301}, // TSR +/ 613 | {"/", 200}, // TSR +/ 614 | {"/PATH", 301}, // Fixed Case 615 | {"/DIR", 301}, // Fixed Case 616 | {"/PATH/", 301}, // Fixed Case -/ 617 | {"/DIR/", 301}, // Fixed Case +/ 618 | {"/paTh/?name=foo", 301}, // Fixed Case With Params +/ 619 | {"/paTh?name=foo", 301}, // Fixed Case With Params +/ 620 | {"/../path", 301}, // CleanPath 621 | {"/nope", 404}, // NotFound 622 | } 623 | 624 | s := &fasthttp.Server{ 625 | Handler: router.Handler, 626 | } 627 | 628 | rw := &readWriter{} 629 | br := bufio.NewReader(&rw.w) 630 | var resp fasthttp.Response 631 | ch := make(chan error) 632 | for _, tr := range testRoutes { 633 | rw.r.WriteString(fmt.Sprintf("GET %s HTTP/1.1\r\n\r\n", tr.route)) 634 | go func() { 635 | ch <- s.ServeConn(rw) 636 | }() 637 | select { 638 | case err := <-ch: 639 | if err != nil { 640 | t.Fatalf("return error %s", err) 641 | } 642 | case <-time.After(100 * time.Millisecond): 643 | t.Fatalf("timeout") 644 | } 645 | if err := resp.Read(br); err != nil { 646 | t.Fatalf("Unexpected error when reading response: %s", err) 647 | } 648 | if !(resp.Header.StatusCode() == tr.code) { 649 | t.Errorf("NotFound handling route %s failed: Code=%d want=%d", 650 | tr.route, resp.Header.StatusCode(), tr.code) 651 | } 652 | } 653 | 654 | // Test custom not found handler 655 | var notFound bool 656 | router.NotFound = fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { 657 | ctx.SetStatusCode(404) 658 | notFound = true 659 | }) 660 | rw.r.WriteString("GET /nope HTTP/1.1\r\n\r\n") 661 | go func() { 662 | ch <- s.ServeConn(rw) 663 | }() 664 | select { 665 | case err := <-ch: 666 | if err != nil { 667 | t.Fatalf("return error %s", err) 668 | } 669 | case <-time.After(100 * time.Millisecond): 670 | t.Fatalf("timeout") 671 | } 672 | if err := resp.Read(br); err != nil { 673 | t.Fatalf("Unexpected error when reading response: %s", err) 674 | } 675 | if !(resp.Header.StatusCode() == 404 && notFound == true) { 676 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", resp.Header.StatusCode(), string(resp.Header.Peek("Location"))) 677 | } 678 | 679 | // Test other method than GET (want 307 instead of 301) 680 | router.PATCH("/path", handlerFunc) 681 | rw.r.WriteString("PATCH /path/ HTTP/1.1\r\n\r\n") 682 | go func() { 683 | ch <- s.ServeConn(rw) 684 | }() 685 | select { 686 | case err := <-ch: 687 | if err != nil { 688 | t.Fatalf("return error %s", err) 689 | } 690 | case <-time.After(100 * time.Millisecond): 691 | t.Fatalf("timeout") 692 | } 693 | if err := resp.Read(br); err != nil { 694 | t.Fatalf("Unexpected error when reading response: %s", err) 695 | } 696 | if !(resp.Header.StatusCode() == 307) { 697 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", resp.Header.StatusCode(), string(resp.Header.Peek("Location"))) 698 | } 699 | 700 | // Test special case where no node for the prefix "/" exists 701 | router = New() 702 | router.GET("/a", handlerFunc) 703 | s.Handler = router.Handler 704 | rw.r.WriteString("GET / HTTP/1.1\r\n\r\n") 705 | go func() { 706 | ch <- s.ServeConn(rw) 707 | }() 708 | select { 709 | case err := <-ch: 710 | if err != nil { 711 | t.Fatalf("return error %s", err) 712 | } 713 | case <-time.After(100 * time.Millisecond): 714 | t.Fatalf("timeout") 715 | } 716 | if err := resp.Read(br); err != nil { 717 | t.Fatalf("Unexpected error when reading response: %s", err) 718 | } 719 | if !(resp.Header.StatusCode() == 404) { 720 | t.Errorf("NotFound handling route / failed: Code=%d", resp.Header.StatusCode()) 721 | } 722 | } 723 | 724 | func TestRouterPanicHandler(t *testing.T) { 725 | router := New() 726 | panicHandled := false 727 | 728 | router.PanicHandler = func(ctx *fasthttp.RequestCtx, p interface{}) { 729 | panicHandled = true 730 | } 731 | 732 | router.Handle("PUT", "/user/:name", func(_ *fasthttp.RequestCtx) { 733 | panic("oops!") 734 | }) 735 | 736 | defer func() { 737 | if rcv := recover(); rcv != nil { 738 | t.Fatal("handling panic failed") 739 | } 740 | }() 741 | 742 | s := &fasthttp.Server{ 743 | Handler: router.Handler, 744 | } 745 | 746 | rw := &readWriter{} 747 | ch := make(chan error) 748 | 749 | rw.r.WriteString(string("PUT /user/gopher HTTP/1.1\r\n\r\n")) 750 | go func() { 751 | ch <- s.ServeConn(rw) 752 | }() 753 | select { 754 | case err := <-ch: 755 | if err != nil { 756 | t.Fatalf("return error %s", err) 757 | } 758 | case <-time.After(100 * time.Millisecond): 759 | t.Fatalf("timeout") 760 | } 761 | 762 | if !panicHandled { 763 | t.Fatal("simulating failed") 764 | } 765 | } 766 | 767 | func TestRouterLookup(t *testing.T) { 768 | routed := false 769 | wantHandle := func(_ *fasthttp.RequestCtx) { 770 | routed = true 771 | } 772 | 773 | router := New() 774 | ctx := &fasthttp.RequestCtx{} 775 | 776 | // try empty router first 777 | handle, tsr := router.Lookup("GET", "/nope", ctx) 778 | if handle != nil { 779 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 780 | } 781 | if tsr { 782 | t.Error("Got wrong TSR recommendation!") 783 | } 784 | 785 | // insert route and try again 786 | router.GET("/user/:name", wantHandle) 787 | 788 | handle, tsr = router.Lookup("GET", "/user/gopher", ctx) 789 | if handle == nil { 790 | t.Fatal("Got no handle!") 791 | } else { 792 | handle(nil) 793 | if !routed { 794 | t.Fatal("Routing failed!") 795 | } 796 | } 797 | if ctx.UserValue("name") != "gopher" { 798 | t.Error("Param not set!") 799 | } 800 | 801 | handle, tsr = router.Lookup("GET", "/user/gopher/", ctx) 802 | if handle != nil { 803 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 804 | } 805 | if !tsr { 806 | t.Error("Got no TSR recommendation!") 807 | } 808 | 809 | handle, tsr = router.Lookup("GET", "/nope", ctx) 810 | if handle != nil { 811 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 812 | } 813 | if tsr { 814 | t.Error("Got wrong TSR recommendation!") 815 | } 816 | } 817 | 818 | type mockFileSystem struct { 819 | opened bool 820 | } 821 | 822 | func (mfs *mockFileSystem) Open(name string) (http.File, error) { 823 | mfs.opened = true 824 | return nil, errors.New("this is just a mock") 825 | } 826 | 827 | func TestRouterServeFiles(t *testing.T) { 828 | router := New() 829 | 830 | recv := catchPanic(func() { 831 | router.ServeFiles("/noFilepath", os.TempDir()) 832 | }) 833 | if recv == nil { 834 | t.Fatal("registering path not ending with '*filepath' did not panic") 835 | } 836 | body := []byte("fake ico") 837 | ioutil.WriteFile(os.TempDir()+"/favicon.ico", body, 0644) 838 | 839 | router.ServeFiles("/*filepath", os.TempDir()) 840 | 841 | s := &fasthttp.Server{ 842 | Handler: router.Handler, 843 | } 844 | 845 | rw := &readWriter{} 846 | ch := make(chan error) 847 | 848 | rw.r.WriteString(string("GET /favicon.ico HTTP/1.1\r\n\r\n")) 849 | go func() { 850 | ch <- s.ServeConn(rw) 851 | }() 852 | select { 853 | case err := <-ch: 854 | if err != nil { 855 | t.Fatalf("return error %s", err) 856 | } 857 | case <-time.After(500 * time.Millisecond): 858 | t.Fatalf("timeout") 859 | } 860 | 861 | br := bufio.NewReader(&rw.w) 862 | var resp fasthttp.Response 863 | if err := resp.Read(br); err != nil { 864 | t.Fatalf("Unexpected error when reading response: %s", err) 865 | } 866 | if resp.Header.StatusCode() != 200 { 867 | t.Fatalf("Unexpected status code %d. Expected %d", resp.Header.StatusCode(), 423) 868 | } 869 | if !bytes.Equal(resp.Body(), body) { 870 | t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), string(body)) 871 | } 872 | } 873 | 874 | type readWriter struct { 875 | net.Conn 876 | r bytes.Buffer 877 | w bytes.Buffer 878 | } 879 | 880 | var zeroTCPAddr = &net.TCPAddr{ 881 | IP: net.IPv4zero, 882 | } 883 | 884 | func (rw *readWriter) Close() error { 885 | return nil 886 | } 887 | 888 | func (rw *readWriter) Read(b []byte) (int, error) { 889 | return rw.r.Read(b) 890 | } 891 | 892 | func (rw *readWriter) Write(b []byte) (int, error) { 893 | return rw.w.Write(b) 894 | } 895 | 896 | func (rw *readWriter) RemoteAddr() net.Addr { 897 | return zeroTCPAddr 898 | } 899 | 900 | func (rw *readWriter) LocalAddr() net.Addr { 901 | return zeroTCPAddr 902 | } 903 | 904 | func (rw *readWriter) SetReadDeadline(t time.Time) error { 905 | return nil 906 | } 907 | 908 | func (rw *readWriter) SetWriteDeadline(t time.Time) error { 909 | return nil 910 | } 911 | -------------------------------------------------------------------------------- /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 fasthttprouter 6 | 7 | import ( 8 | "github.com/valyala/fasthttp" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | func min(a, b int) int { 15 | if a <= b { 16 | return a 17 | } 18 | return b 19 | } 20 | 21 | func countParams(path string) uint8 { 22 | var n uint 23 | for i := 0; i < len(path); i++ { 24 | if path[i] != ':' && path[i] != '*' { 25 | continue 26 | } 27 | n++ 28 | } 29 | if n >= 255 { 30 | return 255 31 | } 32 | return uint8(n) 33 | } 34 | 35 | type nodeType uint8 36 | 37 | const ( 38 | static nodeType = iota // default 39 | root 40 | param 41 | catchAll 42 | ) 43 | 44 | type node struct { 45 | path string 46 | wildChild bool 47 | nType nodeType 48 | maxParams uint8 49 | indices string 50 | children []*node 51 | handle fasthttp.RequestHandler 52 | priority uint32 53 | } 54 | 55 | // increments priority of the given child and reorders if necessary 56 | func (n *node) incrementChildPrio(pos int) int { 57 | n.children[pos].priority++ 58 | prio := n.children[pos].priority 59 | 60 | // adjust position (move to front) 61 | newPos := pos 62 | for newPos > 0 && n.children[newPos-1].priority < prio { 63 | // swap node positions 64 | tmpN := n.children[newPos-1] 65 | n.children[newPos-1] = n.children[newPos] 66 | n.children[newPos] = tmpN 67 | 68 | newPos-- 69 | } 70 | 71 | // build new index char string 72 | if newPos != pos { 73 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty 74 | n.indices[pos:pos+1] + // the index char we move 75 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' 76 | } 77 | 78 | return newPos 79 | } 80 | 81 | // addRoute adds a node with the given handle to the path. 82 | // Not concurrency-safe! 83 | func (n *node) addRoute(path string, handle fasthttp.RequestHandler) { 84 | fullPath := path 85 | n.priority++ 86 | numParams := countParams(path) 87 | 88 | // non-empty tree 89 | if len(n.path) > 0 || len(n.children) > 0 { 90 | walk: 91 | for { 92 | // Update maxParams of the current node 93 | if numParams > n.maxParams { 94 | n.maxParams = numParams 95 | } 96 | 97 | // Find the longest common prefix. 98 | // This also implies that the common prefix contains no ':' or '*' 99 | // since the existing key can't contain those chars. 100 | i := 0 101 | max := min(len(path), len(n.path)) 102 | for i < max && path[i] == n.path[i] { 103 | i++ 104 | } 105 | 106 | // Split edge 107 | if i < len(n.path) { 108 | child := node{ 109 | path: n.path[i:], 110 | wildChild: n.wildChild, 111 | nType: static, 112 | indices: n.indices, 113 | children: n.children, 114 | handle: n.handle, 115 | priority: n.priority - 1, 116 | } 117 | 118 | // Update maxParams (max of all children) 119 | for i := range child.children { 120 | if child.children[i].maxParams > child.maxParams { 121 | child.maxParams = child.children[i].maxParams 122 | } 123 | } 124 | 125 | n.children = []*node{&child} 126 | // []byte for proper unicode char conversion, see #65 127 | n.indices = string([]byte{n.path[i]}) 128 | n.path = path[:i] 129 | n.handle = nil 130 | n.wildChild = false 131 | } 132 | 133 | // Make new node a child of this node 134 | if i < len(path) { 135 | path = path[i:] 136 | 137 | if n.wildChild { 138 | n = n.children[0] 139 | n.priority++ 140 | 141 | // Update maxParams of the child node 142 | if numParams > n.maxParams { 143 | n.maxParams = numParams 144 | } 145 | numParams-- 146 | 147 | // Check if the wildcard matches 148 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] && 149 | // Check for longer wildcard, e.g. :name and :names 150 | (len(n.path) >= len(path) || path[len(n.path)] == '/') { 151 | continue walk 152 | } else { 153 | // Wildcard conflict 154 | pathSeg := strings.SplitN(path, "/", 2)[0] 155 | prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path 156 | panic("'" + pathSeg + 157 | "' in new path '" + fullPath + 158 | "' conflicts with existing wildcard '" + n.path + 159 | "' in existing prefix '" + prefix + 160 | "'") 161 | } 162 | } 163 | 164 | c := path[0] 165 | 166 | // slash after param 167 | if n.nType == param && c == '/' && len(n.children) == 1 { 168 | n = n.children[0] 169 | n.priority++ 170 | continue walk 171 | } 172 | 173 | // Check if a child with the next path byte exists 174 | for i := 0; i < len(n.indices); i++ { 175 | if c == n.indices[i] { 176 | i = n.incrementChildPrio(i) 177 | n = n.children[i] 178 | continue walk 179 | } 180 | } 181 | 182 | // Otherwise insert it 183 | if c != ':' && c != '*' { 184 | // []byte for proper unicode char conversion, see #65 185 | n.indices += string([]byte{c}) 186 | child := &node{ 187 | maxParams: numParams, 188 | } 189 | n.children = append(n.children, child) 190 | n.incrementChildPrio(len(n.indices) - 1) 191 | n = child 192 | } 193 | n.insertChild(numParams, path, fullPath, handle) 194 | return 195 | 196 | } else if i == len(path) { // Make node a (in-path) leaf 197 | if n.handle != nil { 198 | panic("a handle is already registered for path '" + fullPath + "'") 199 | } 200 | n.handle = handle 201 | } 202 | return 203 | } 204 | } else { // Empty tree 205 | n.insertChild(numParams, path, fullPath, handle) 206 | n.nType = root 207 | } 208 | } 209 | 210 | func (n *node) insertChild(numParams uint8, path, fullPath string, handle fasthttp.RequestHandler) { 211 | var offset int // already handled bytes of the path 212 | 213 | // find prefix until first wildcard (beginning with ':'' or '*'') 214 | for i, max := 0, len(path); numParams > 0; i++ { 215 | c := path[i] 216 | if c != ':' && c != '*' { 217 | continue 218 | } 219 | 220 | // find wildcard end (either '/' or path end) 221 | end := i + 1 222 | for end < max && path[end] != '/' { 223 | switch path[end] { 224 | // the wildcard name must not contain ':' and '*' 225 | case ':', '*': 226 | panic("only one wildcard per path segment is allowed, has: '" + 227 | path[i:] + "' in path '" + fullPath + "'") 228 | default: 229 | end++ 230 | } 231 | } 232 | 233 | // check if this Node existing children which would be 234 | // unreachable if we insert the wildcard here 235 | if len(n.children) > 0 { 236 | panic("wildcard route '" + path[i:end] + 237 | "' conflicts with existing children in path '" + fullPath + "'") 238 | } 239 | 240 | // check if the wildcard has a name 241 | if end-i < 2 { 242 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 243 | } 244 | 245 | if c == ':' { // param 246 | // split path at the beginning of the wildcard 247 | if i > 0 { 248 | n.path = path[offset:i] 249 | offset = i 250 | } 251 | 252 | child := &node{ 253 | nType: param, 254 | maxParams: numParams, 255 | } 256 | n.children = []*node{child} 257 | n.wildChild = true 258 | n = child 259 | n.priority++ 260 | numParams-- 261 | 262 | // if the path doesn't end with the wildcard, then there 263 | // will be another non-wildcard subpath starting with '/' 264 | if end < max { 265 | n.path = path[offset:end] 266 | offset = end 267 | 268 | child := &node{ 269 | maxParams: numParams, 270 | priority: 1, 271 | } 272 | n.children = []*node{child} 273 | n = child 274 | } 275 | 276 | } else { // catchAll 277 | if end != max || numParams > 1 { 278 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 279 | } 280 | 281 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 282 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 283 | } 284 | 285 | // currently fixed width 1 for '/' 286 | i-- 287 | if path[i] != '/' { 288 | panic("no / before catch-all in path '" + fullPath + "'") 289 | } 290 | 291 | n.path = path[offset:i] 292 | 293 | // first node: catchAll node with empty path 294 | child := &node{ 295 | wildChild: true, 296 | nType: catchAll, 297 | maxParams: 1, 298 | } 299 | n.children = []*node{child} 300 | n.indices = string(path[i]) 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 | maxParams: 1, 309 | handle: handle, 310 | priority: 1, 311 | } 312 | n.children = []*node{child} 313 | 314 | return 315 | } 316 | } 317 | 318 | // insert remaining path part and handle to the leaf 319 | n.path = path[offset:] 320 | n.handle = handle 321 | } 322 | 323 | // Returns the handle registered with the given path (key). The values of 324 | // wildcards are saved to a map. 325 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is 326 | // made if a handle exists with an extra (without the) trailing slash for the 327 | // given path. 328 | func (n *node) getValue(path string, ctx *fasthttp.RequestCtx) (handle fasthttp.RequestHandler, tsr bool) { 329 | walk: // outer loop for walking the tree 330 | for { 331 | if len(path) > len(n.path) { 332 | if path[:len(n.path)] == n.path { 333 | path = path[len(n.path):] 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 | c := path[0] 339 | for i := 0; i < len(n.indices); i++ { 340 | if c == n.indices[i] { 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 | 354 | // handle wildcard child 355 | n = n.children[0] 356 | switch n.nType { 357 | case param: 358 | // find param end (either '/' or path end) 359 | end := 0 360 | for end < len(path) && path[end] != '/' { 361 | end++ 362 | } 363 | 364 | // handle calls to Router.allowed method with nil context 365 | if ctx != nil { 366 | ctx.SetUserValue(n.path[1:], path[:end]) 367 | } 368 | 369 | // we need to go deeper! 370 | if end < len(path) { 371 | if len(n.children) > 0 { 372 | path = path[end:] 373 | n = n.children[0] 374 | continue walk 375 | } 376 | 377 | // ... but we can't 378 | tsr = (len(path) == end+1) 379 | return 380 | } 381 | 382 | if handle = n.handle; handle != nil { 383 | return 384 | } else if len(n.children) == 1 { 385 | // No handle found. Check if a handle for this path + a 386 | // trailing slash exists for TSR recommendation 387 | n = n.children[0] 388 | tsr = (n.path == "/" && n.handle != nil) 389 | } 390 | 391 | return 392 | 393 | case catchAll: 394 | if ctx != nil { 395 | // save param value 396 | ctx.SetUserValue(n.path[2:], path) 397 | } 398 | handle = n.handle 399 | return 400 | 401 | default: 402 | panic("invalid node type") 403 | } 404 | } 405 | } else if path == n.path { 406 | // We should have reached the node containing the handle. 407 | // Check if this node has a handle registered. 408 | if handle = n.handle; handle != nil { 409 | return 410 | } 411 | 412 | if path == "/" && n.wildChild && n.nType != root { 413 | tsr = true 414 | return 415 | } 416 | 417 | // No handle found. Check if a handle for this path + a 418 | // trailing slash exists for trailing slash recommendation 419 | for i := 0; i < len(n.indices); i++ { 420 | if n.indices[i] == '/' { 421 | n = n.children[i] 422 | tsr = (len(n.path) == 1 && n.handle != nil) || 423 | (n.nType == catchAll && n.children[0].handle != nil) 424 | return 425 | } 426 | } 427 | 428 | return 429 | } 430 | 431 | // Nothing found. We can recommend to redirect to the same URL with an 432 | // extra trailing slash if a leaf exists for that path 433 | tsr = (path == "/") || 434 | (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && 435 | path == n.path[:len(n.path)-1] && n.handle != nil) 436 | return 437 | } 438 | } 439 | 440 | // Makes a case-insensitive lookup of the given path and tries to find a handler. 441 | // It can optionally also fix trailing slashes. 442 | // It returns the case-corrected path and a bool indicating whether the lookup 443 | // was successful. 444 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) { 445 | return n.findCaseInsensitivePathRec( 446 | path, 447 | strings.ToLower(path), 448 | make([]byte, 0, len(path)+1), // preallocate enough memory for new path 449 | [4]byte{}, // empty rune buffer 450 | fixTrailingSlash, 451 | ) 452 | } 453 | 454 | // shift bytes in array by n bytes left 455 | func shiftNRuneBytes(rb [4]byte, n int) [4]byte { 456 | switch n { 457 | case 0: 458 | return rb 459 | case 1: 460 | return [4]byte{rb[1], rb[2], rb[3], 0} 461 | case 2: 462 | return [4]byte{rb[2], rb[3]} 463 | case 3: 464 | return [4]byte{rb[3]} 465 | default: 466 | return [4]byte{} 467 | } 468 | } 469 | 470 | // recursive case-insensitive lookup function used by n.findCaseInsensitivePath 471 | func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { 472 | loNPath := strings.ToLower(n.path) 473 | 474 | walk: // outer loop for walking the tree 475 | for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) { 476 | // add common path to result 477 | ciPath = append(ciPath, n.path...) 478 | 479 | if path = path[len(n.path):]; len(path) > 0 { 480 | loOld := loPath 481 | loPath = loPath[len(loNPath):] 482 | 483 | // If this node does not have a wildcard (param or catchAll) child, 484 | // we can just look up the next child node and continue to walk down 485 | // the tree 486 | if !n.wildChild { 487 | // skip rune bytes already processed 488 | rb = shiftNRuneBytes(rb, len(loNPath)) 489 | 490 | if rb[0] != 0 { 491 | // old rune not finished 492 | for i := 0; i < len(n.indices); i++ { 493 | if n.indices[i] == rb[0] { 494 | // continue with child node 495 | n = n.children[i] 496 | loNPath = strings.ToLower(n.path) 497 | continue walk 498 | } 499 | } 500 | } else { 501 | // process a new rune 502 | var rv rune 503 | 504 | // find rune start 505 | // runes are up to 4 byte long, 506 | // -4 would definitely be another rune 507 | var off int 508 | for max := min(len(loNPath), 3); off < max; off++ { 509 | if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) { 510 | // read rune from cached lowercase path 511 | rv, _ = utf8.DecodeRuneInString(loOld[i:]) 512 | break 513 | } 514 | } 515 | 516 | // calculate lowercase bytes of current rune 517 | utf8.EncodeRune(rb[:], rv) 518 | // skipp already processed bytes 519 | rb = shiftNRuneBytes(rb, off) 520 | 521 | for i := 0; i < len(n.indices); i++ { 522 | // lowercase matches 523 | if n.indices[i] == rb[0] { 524 | // must use a recursive approach since both the 525 | // uppercase byte and the lowercase byte might exist 526 | // as an index 527 | if out, found := n.children[i].findCaseInsensitivePathRec( 528 | path, loPath, ciPath, rb, fixTrailingSlash, 529 | ); found { 530 | return out, true 531 | } 532 | break 533 | } 534 | } 535 | 536 | // same for uppercase rune, if it differs 537 | if up := unicode.ToUpper(rv); up != rv { 538 | utf8.EncodeRune(rb[:], up) 539 | rb = shiftNRuneBytes(rb, off) 540 | 541 | for i := 0; i < len(n.indices); i++ { 542 | // uppercase matches 543 | if n.indices[i] == rb[0] { 544 | // continue with child node 545 | n = n.children[i] 546 | loNPath = strings.ToLower(n.path) 547 | continue walk 548 | } 549 | } 550 | } 551 | } 552 | 553 | // Nothing found. We can recommend to redirect to the same URL 554 | // without a trailing slash if a leaf exists for that path 555 | return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil) 556 | } 557 | 558 | n = n.children[0] 559 | switch n.nType { 560 | case param: 561 | // find param end (either '/' or path end) 562 | k := 0 563 | for k < len(path) && path[k] != '/' { 564 | k++ 565 | } 566 | 567 | // add param value to case insensitive path 568 | ciPath = append(ciPath, path[:k]...) 569 | 570 | // we need to go deeper! 571 | if k < len(path) { 572 | if len(n.children) > 0 { 573 | // continue with child node 574 | n = n.children[0] 575 | loNPath = strings.ToLower(n.path) 576 | loPath = loPath[k:] 577 | path = path[k:] 578 | continue 579 | } 580 | 581 | // ... but we can't 582 | if fixTrailingSlash && len(path) == k+1 { 583 | return ciPath, true 584 | } 585 | return ciPath, false 586 | } 587 | 588 | if n.handle != nil { 589 | return ciPath, true 590 | } else if fixTrailingSlash && len(n.children) == 1 { 591 | // No handle found. Check if a handle for this path + a 592 | // trailing slash exists 593 | n = n.children[0] 594 | if n.path == "/" && n.handle != nil { 595 | return append(ciPath, '/'), true 596 | } 597 | } 598 | return ciPath, false 599 | 600 | case catchAll: 601 | return append(ciPath, path...), true 602 | 603 | default: 604 | panic("invalid node type") 605 | } 606 | } else { 607 | // We should have reached the node containing the handle. 608 | // Check if this node has a handle registered. 609 | if n.handle != nil { 610 | return ciPath, true 611 | } 612 | 613 | // No handle found. 614 | // Try to fix the path by adding a trailing slash 615 | if fixTrailingSlash { 616 | for i := 0; i < len(n.indices); i++ { 617 | if n.indices[i] == '/' { 618 | n = n.children[i] 619 | if (len(n.path) == 1 && n.handle != nil) || 620 | (n.nType == catchAll && n.children[0].handle != nil) { 621 | return append(ciPath, '/'), true 622 | } 623 | return ciPath, false 624 | } 625 | } 626 | } 627 | return ciPath, false 628 | } 629 | } 630 | 631 | // Nothing found. 632 | // Try to fix the path by adding / removing a trailing slash 633 | if fixTrailingSlash { 634 | if path == "/" { 635 | return ciPath, true 636 | } 637 | if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' && 638 | loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil { 639 | return append(ciPath, n.path...), true 640 | } 641 | } 642 | return ciPath, false 643 | } 644 | -------------------------------------------------------------------------------- /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 fasthttprouter 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | func printChildren(n *node, prefix string) { 16 | fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) 17 | for l := len(n.path); l > 0; l-- { 18 | prefix += " " 19 | } 20 | for _, child := range n.children { 21 | printChildren(child, prefix) 22 | } 23 | } 24 | 25 | // Used as a workaround since we can't compare functions or their addresses 26 | var fakeHandlerValue string 27 | 28 | func fakeHandler(val string) fasthttp.RequestHandler { 29 | return func(*fasthttp.RequestCtx) { 30 | fakeHandlerValue = val 31 | } 32 | } 33 | 34 | type testRequests []struct { 35 | path string 36 | nilHandler bool 37 | route string 38 | ps map[string]string 39 | } 40 | 41 | func acquarieReqeustCtx(path string) *fasthttp.RequestCtx { 42 | var requestCtx fasthttp.RequestCtx 43 | var fastRequest fasthttp.Request 44 | fastRequest.SetRequestURI(path) 45 | requestCtx.Init(&fastRequest, nil, nil) 46 | return &requestCtx 47 | } 48 | 49 | func checkRequests(t *testing.T, tree *node, requests testRequests) { 50 | for _, request := range requests { 51 | requestCtx := acquarieReqeustCtx(request.path) 52 | handler, _ := tree.getValue(request.path, requestCtx) 53 | 54 | if handler == nil { 55 | if !request.nilHandler { 56 | t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) 57 | } 58 | } else if request.nilHandler { 59 | t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) 60 | } else { 61 | handler(nil) 62 | if fakeHandlerValue != request.route { 63 | t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) 64 | } 65 | } 66 | 67 | for expectedKey, expectedVal := range request.ps { 68 | if requestCtx.UserValue(expectedKey) != expectedVal { 69 | t.Errorf(" mismatch for route '%s'", request.path) 70 | } 71 | } 72 | } 73 | } 74 | 75 | func checkPriorities(t *testing.T, n *node) uint32 { 76 | var prio uint32 77 | for i := range n.children { 78 | prio += checkPriorities(t, n.children[i]) 79 | } 80 | 81 | if n.handle != nil { 82 | prio++ 83 | } 84 | 85 | if n.priority != prio { 86 | t.Errorf( 87 | "priority mismatch for node '%s': is %d, should be %d", 88 | n.path, n.priority, prio, 89 | ) 90 | } 91 | 92 | return prio 93 | } 94 | 95 | func checkMaxParams(t *testing.T, n *node) uint8 { 96 | var maxParams uint8 97 | for i := range n.children { 98 | params := checkMaxParams(t, n.children[i]) 99 | if params > maxParams { 100 | maxParams = params 101 | } 102 | } 103 | if n.nType > root && !n.wildChild { 104 | maxParams++ 105 | } 106 | 107 | if n.maxParams != maxParams { 108 | t.Errorf( 109 | "maxParams mismatch for node '%s': is %d, should be %d", 110 | n.path, n.maxParams, maxParams, 111 | ) 112 | } 113 | 114 | return maxParams 115 | } 116 | 117 | func TestCountParams(t *testing.T) { 118 | if countParams("/path/:param1/static/*catch-all") != 2 { 119 | t.Fail() 120 | } 121 | if countParams(strings.Repeat("/:param", 256)) != 255 { 122 | t.Fail() 123 | } 124 | } 125 | 126 | func TestTreeAddAndGet(t *testing.T) { 127 | tree := &node{} 128 | 129 | routes := [...]string{ 130 | "/hi", 131 | "/contact", 132 | "/co", 133 | "/c", 134 | "/a", 135 | "/ab", 136 | "/doc/", 137 | "/doc/go_faq.html", 138 | "/doc/go1.html", 139 | "/α", 140 | "/β", 141 | } 142 | for _, route := range routes { 143 | tree.addRoute(route, fakeHandler(route)) 144 | } 145 | 146 | //printChildren(tree, "") 147 | 148 | checkRequests(t, tree, testRequests{ 149 | {"/a", false, "/a", nil}, 150 | {"/", true, "", nil}, 151 | {"/hi", false, "/hi", nil}, 152 | {"/contact", false, "/contact", nil}, 153 | {"/co", false, "/co", nil}, 154 | {"/con", true, "", nil}, // key mismatch 155 | {"/cona", true, "", nil}, // key mismatch 156 | {"/no", true, "", nil}, // no matching child 157 | {"/ab", false, "/ab", nil}, 158 | {"/α", false, "/α", nil}, 159 | {"/β", false, "/β", nil}, 160 | }) 161 | 162 | checkPriorities(t, tree) 163 | checkMaxParams(t, tree) 164 | } 165 | 166 | func TestTreeWildcard(t *testing.T) { 167 | tree := &node{} 168 | 169 | routes := [...]string{ 170 | "/", 171 | "/cmd/:tool/:sub", 172 | "/cmd/:tool/", 173 | "/src/*filepath", 174 | "/search/", 175 | "/search/:query", 176 | "/user_:name", 177 | "/user_:name/about", 178 | "/files/:dir/*filepath", 179 | "/doc/", 180 | "/doc/go_faq.html", 181 | "/doc/go1.html", 182 | "/info/:user/public", 183 | "/info/:user/project/:project", 184 | } 185 | for _, route := range routes { 186 | tree.addRoute(route, fakeHandler(route)) 187 | } 188 | 189 | //printChildren(tree, "") 190 | 191 | checkRequests(t, tree, testRequests{ 192 | {"/", false, "/", nil}, 193 | {"/cmd/test/", false, "/cmd/:tool/", map[string]string{"tool": "test"}}, 194 | {"/cmd/test", true, "", map[string]string{"tool": "test"}}, 195 | {"/cmd/test/3", false, "/cmd/:tool/:sub", map[string]string{"tool": "test", "sub": "3"}}, 196 | {"/src/", false, "/src/*filepath", map[string]string{"filepath": "/"}}, 197 | {"/src/some/file.png", false, "/src/*filepath", map[string]string{"filepath": "/some/file.png"}}, 198 | {"/search/", false, "/search/", nil}, 199 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", map[string]string{"query": "someth!ng+in+ünìcodé"}}, 200 | {"/search/someth!ng+in+ünìcodé/", true, "", map[string]string{"query": "someth!ng+in+ünìcodé"}}, 201 | {"/user_gopher", false, "/user_:name", map[string]string{"name": "gopher"}}, 202 | {"/user_gopher/about", false, "/user_:name/about", map[string]string{"name": "gopher"}}, 203 | {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", map[string]string{"dir": "js", "filepath": "/inc/framework.js"}}, 204 | {"/info/gordon/public", false, "/info/:user/public", map[string]string{"user": "gordon"}}, 205 | {"/info/gordon/project/go", false, "/info/:user/project/:project", map[string]string{"user": "gordon", "project": "go"}}, 206 | }) 207 | 208 | checkPriorities(t, tree) 209 | checkMaxParams(t, tree) 210 | } 211 | 212 | func catchPanic(testFunc func()) (recv interface{}) { 213 | defer func() { 214 | recv = recover() 215 | }() 216 | 217 | testFunc() 218 | return 219 | } 220 | 221 | type testRoute struct { 222 | path string 223 | conflict bool 224 | } 225 | 226 | func testRoutes(t *testing.T, routes []testRoute) { 227 | tree := &node{} 228 | 229 | for _, route := range routes { 230 | recv := catchPanic(func() { 231 | tree.addRoute(route.path, nil) 232 | }) 233 | 234 | if route.conflict { 235 | if recv == nil { 236 | t.Errorf("no panic for conflicting route '%s'", route.path) 237 | } 238 | } else if recv != nil { 239 | t.Errorf("unexpected panic for route '%s': %v", route.path, recv) 240 | } 241 | } 242 | 243 | //printChildren(tree, "") 244 | } 245 | 246 | func TestTreeWildcardConflict(t *testing.T) { 247 | routes := []testRoute{ 248 | {"/cmd/:tool/:sub", false}, 249 | {"/cmd/vet", true}, 250 | {"/src/*filepath", false}, 251 | {"/src/*filepathx", true}, 252 | {"/src/", true}, 253 | {"/src1/", false}, 254 | {"/src1/*filepath", true}, 255 | {"/src2*filepath", true}, 256 | {"/search/:query", false}, 257 | {"/search/invalid", true}, 258 | {"/user_:name", false}, 259 | {"/user_x", true}, 260 | {"/user_:name", false}, 261 | {"/id:id", false}, 262 | {"/id/:id", true}, 263 | } 264 | testRoutes(t, routes) 265 | } 266 | 267 | func TestTreeChildConflict(t *testing.T) { 268 | routes := []testRoute{ 269 | {"/cmd/vet", false}, 270 | {"/cmd/:tool/:sub", true}, 271 | {"/src/AUTHORS", false}, 272 | {"/src/*filepath", true}, 273 | {"/user_x", false}, 274 | {"/user_:name", true}, 275 | {"/id/:id", false}, 276 | {"/id:id", true}, 277 | {"/:id", true}, 278 | {"/*filepath", true}, 279 | } 280 | testRoutes(t, routes) 281 | } 282 | 283 | func TestTreeDupliatePath(t *testing.T) { 284 | tree := &node{} 285 | 286 | routes := [...]string{ 287 | "/", 288 | "/doc/", 289 | "/src/*filepath", 290 | "/search/:query", 291 | "/user_:name", 292 | } 293 | for _, route := range routes { 294 | recv := catchPanic(func() { 295 | tree.addRoute(route, fakeHandler(route)) 296 | }) 297 | if recv != nil { 298 | t.Fatalf("panic inserting route '%s': %v", route, recv) 299 | } 300 | 301 | // Add again 302 | recv = catchPanic(func() { 303 | tree.addRoute(route, nil) 304 | }) 305 | if recv == nil { 306 | t.Fatalf("no panic while inserting duplicate route '%s", route) 307 | } 308 | } 309 | 310 | //printChildren(tree, "") 311 | 312 | checkRequests(t, tree, testRequests{ 313 | {"/", false, "/", nil}, 314 | {"/doc/", false, "/doc/", nil}, 315 | {"/src/some/file.png", false, "/src/*filepath", map[string]string{"filepath": "/some/file.png"}}, 316 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", map[string]string{"query": "someth!ng+in+ünìcodé"}}, 317 | {"/user_gopher", false, "/user_:name", map[string]string{"name": "gopher"}}, 318 | }) 319 | } 320 | 321 | func TestEmptyWildcardName(t *testing.T) { 322 | tree := &node{} 323 | 324 | routes := [...]string{ 325 | "/user:", 326 | "/user:/", 327 | "/cmd/:/", 328 | "/src/*", 329 | } 330 | for _, route := range routes { 331 | recv := catchPanic(func() { 332 | tree.addRoute(route, nil) 333 | }) 334 | if recv == nil { 335 | t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) 336 | } 337 | } 338 | } 339 | 340 | func TestTreeCatchAllConflict(t *testing.T) { 341 | routes := []testRoute{ 342 | {"/src/*filepath/x", true}, 343 | {"/src2/", false}, 344 | {"/src2/*filepath/x", true}, 345 | } 346 | testRoutes(t, routes) 347 | } 348 | 349 | func TestTreeCatchAllConflictRoot(t *testing.T) { 350 | routes := []testRoute{ 351 | {"/", false}, 352 | {"/*filepath", true}, 353 | } 354 | testRoutes(t, routes) 355 | } 356 | 357 | func TestTreeDoubleWildcard(t *testing.T) { 358 | const panicMsg = "only one wildcard per path segment is allowed" 359 | 360 | routes := [...]string{ 361 | "/:foo:bar", 362 | "/:foo:bar/", 363 | "/:foo*bar", 364 | } 365 | 366 | for _, route := range routes { 367 | tree := &node{} 368 | recv := catchPanic(func() { 369 | tree.addRoute(route, nil) 370 | }) 371 | 372 | if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { 373 | t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) 374 | } 375 | } 376 | } 377 | 378 | /*func TestTreeDuplicateWildcard(t *testing.T) { 379 | tree := &node{} 380 | 381 | routes := [...]string{ 382 | "/:id/:name/:id", 383 | } 384 | for _, route := range routes { 385 | ... 386 | } 387 | }*/ 388 | 389 | func TestTreeTrailingSlashRedirect(t *testing.T) { 390 | tree := &node{} 391 | ctx := &fasthttp.RequestCtx{} 392 | 393 | routes := [...]string{ 394 | "/hi", 395 | "/b/", 396 | "/search/:query", 397 | "/cmd/:tool/", 398 | "/src/*filepath", 399 | "/x", 400 | "/x/y", 401 | "/y/", 402 | "/y/z", 403 | "/0/:id", 404 | "/0/:id/1", 405 | "/1/:id/", 406 | "/1/:id/2", 407 | "/aa", 408 | "/a/", 409 | "/admin", 410 | "/admin/:category", 411 | "/admin/:category/:page", 412 | "/doc", 413 | "/doc/go_faq.html", 414 | "/doc/go1.html", 415 | "/no/a", 416 | "/no/b", 417 | "/api/hello/:name", 418 | } 419 | for _, route := range routes { 420 | recv := catchPanic(func() { 421 | tree.addRoute(route, fakeHandler(route)) 422 | }) 423 | if recv != nil { 424 | t.Fatalf("panic inserting route '%s': %v", route, recv) 425 | } 426 | } 427 | 428 | //printChildren(tree, "") 429 | 430 | tsrRoutes := [...]string{ 431 | "/hi/", 432 | "/b", 433 | "/search/gopher/", 434 | "/cmd/vet", 435 | "/src", 436 | "/x/", 437 | "/y", 438 | "/0/go/", 439 | "/1/go", 440 | "/a", 441 | "/admin/", 442 | "/admin/config/", 443 | "/admin/config/permissions/", 444 | "/doc/", 445 | } 446 | for _, route := range tsrRoutes { 447 | handler, tsr := tree.getValue(route, ctx) 448 | if handler != nil { 449 | t.Fatalf("non-nil handler for TSR route '%s", route) 450 | } else if !tsr { 451 | t.Errorf("expected TSR recommendation for route '%s'", route) 452 | } 453 | } 454 | 455 | noTsrRoutes := [...]string{ 456 | "/", 457 | "/no", 458 | "/no/", 459 | "/_", 460 | "/_/", 461 | "/api/world/abc", 462 | } 463 | for _, route := range noTsrRoutes { 464 | handler, tsr := tree.getValue(route, ctx) 465 | if handler != nil { 466 | t.Fatalf("non-nil handler for No-TSR route '%s", route) 467 | } else if tsr { 468 | t.Errorf("expected no TSR recommendation for route '%s'", route) 469 | } 470 | } 471 | } 472 | 473 | func TestTreeRootTrailingSlashRedirect(t *testing.T) { 474 | tree := &node{} 475 | 476 | recv := catchPanic(func() { 477 | tree.addRoute("/:test", fakeHandler("/:test")) 478 | }) 479 | if recv != nil { 480 | t.Fatalf("panic inserting test route: %v", recv) 481 | } 482 | 483 | handler, tsr := tree.getValue("/", nil) 484 | if handler != nil { 485 | t.Fatalf("non-nil handler") 486 | } else if tsr { 487 | t.Errorf("expected no TSR recommendation") 488 | } 489 | } 490 | 491 | func TestTreeFindCaseInsensitivePath(t *testing.T) { 492 | tree := &node{} 493 | 494 | routes := [...]string{ 495 | "/hi", 496 | "/b/", 497 | "/ABC/", 498 | "/search/:query", 499 | "/cmd/:tool/", 500 | "/src/*filepath", 501 | "/x", 502 | "/x/y", 503 | "/y/", 504 | "/y/z", 505 | "/0/:id", 506 | "/0/:id/1", 507 | "/1/:id/", 508 | "/1/:id/2", 509 | "/aa", 510 | "/a/", 511 | "/doc", 512 | "/doc/go_faq.html", 513 | "/doc/go1.html", 514 | "/doc/go/away", 515 | "/no/a", 516 | "/no/b", 517 | "/Π", 518 | "/u/apfêl/", 519 | "/u/äpfêl/", 520 | "/u/öpfêl", 521 | "/v/Äpfêl/", 522 | "/v/Öpfêl", 523 | "/w/♬", // 3 byte 524 | "/w/♭/", // 3 byte, last byte differs 525 | "/w/𠜎", // 4 byte 526 | "/w/𠜏/", // 4 byte 527 | } 528 | 529 | for _, route := range routes { 530 | recv := catchPanic(func() { 531 | tree.addRoute(route, fakeHandler(route)) 532 | }) 533 | if recv != nil { 534 | t.Fatalf("panic inserting route '%s': %v", route, recv) 535 | } 536 | } 537 | 538 | // Check out == in for all registered routes 539 | // With fixTrailingSlash = true 540 | for _, route := range routes { 541 | out, found := tree.findCaseInsensitivePath(route, true) 542 | if !found { 543 | t.Errorf("Route '%s' not found!", route) 544 | } else if string(out) != route { 545 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) 546 | } 547 | } 548 | // With fixTrailingSlash = false 549 | for _, route := range routes { 550 | out, found := tree.findCaseInsensitivePath(route, false) 551 | if !found { 552 | t.Errorf("Route '%s' not found!", route) 553 | } else if string(out) != route { 554 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) 555 | } 556 | } 557 | 558 | tests := []struct { 559 | in string 560 | out string 561 | found bool 562 | slash bool 563 | }{ 564 | {"/HI", "/hi", true, false}, 565 | {"/HI/", "/hi", true, true}, 566 | {"/B", "/b/", true, true}, 567 | {"/B/", "/b/", true, false}, 568 | {"/abc", "/ABC/", true, true}, 569 | {"/abc/", "/ABC/", true, false}, 570 | {"/aBc", "/ABC/", true, true}, 571 | {"/aBc/", "/ABC/", true, false}, 572 | {"/abC", "/ABC/", true, true}, 573 | {"/abC/", "/ABC/", true, false}, 574 | {"/SEARCH/QUERY", "/search/QUERY", true, false}, 575 | {"/SEARCH/QUERY/", "/search/QUERY", true, true}, 576 | {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, 577 | {"/CMD/TOOL", "/cmd/TOOL/", true, true}, 578 | {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, 579 | {"/x/Y", "/x/y", true, false}, 580 | {"/x/Y/", "/x/y", true, true}, 581 | {"/X/y", "/x/y", true, false}, 582 | {"/X/y/", "/x/y", true, true}, 583 | {"/X/Y", "/x/y", true, false}, 584 | {"/X/Y/", "/x/y", true, true}, 585 | {"/Y/", "/y/", true, false}, 586 | {"/Y", "/y/", true, true}, 587 | {"/Y/z", "/y/z", true, false}, 588 | {"/Y/z/", "/y/z", true, true}, 589 | {"/Y/Z", "/y/z", true, false}, 590 | {"/Y/Z/", "/y/z", true, true}, 591 | {"/y/Z", "/y/z", true, false}, 592 | {"/y/Z/", "/y/z", true, true}, 593 | {"/Aa", "/aa", true, false}, 594 | {"/Aa/", "/aa", true, true}, 595 | {"/AA", "/aa", true, false}, 596 | {"/AA/", "/aa", true, true}, 597 | {"/aA", "/aa", true, false}, 598 | {"/aA/", "/aa", true, true}, 599 | {"/A/", "/a/", true, false}, 600 | {"/A", "/a/", true, true}, 601 | {"/DOC", "/doc", true, false}, 602 | {"/DOC/", "/doc", true, true}, 603 | {"/NO", "", false, true}, 604 | {"/DOC/GO", "", false, true}, 605 | {"/π", "/Π", true, false}, 606 | {"/π/", "/Π", true, true}, 607 | {"/u/ÄPFÊL/", "/u/äpfêl/", true, false}, 608 | {"/u/ÄPFÊL", "/u/äpfêl/", true, true}, 609 | {"/u/ÖPFÊL/", "/u/öpfêl", true, true}, 610 | {"/u/ÖPFÊL", "/u/öpfêl", true, false}, 611 | {"/v/äpfêL/", "/v/Äpfêl/", true, false}, 612 | {"/v/äpfêL", "/v/Äpfêl/", true, true}, 613 | {"/v/öpfêL/", "/v/Öpfêl", true, true}, 614 | {"/v/öpfêL", "/v/Öpfêl", true, false}, 615 | {"/w/♬/", "/w/♬", true, true}, 616 | {"/w/♭", "/w/♭/", true, true}, 617 | {"/w/𠜎/", "/w/𠜎", true, true}, 618 | {"/w/𠜏", "/w/𠜏/", true, true}, 619 | } 620 | // With fixTrailingSlash = true 621 | for _, test := range tests { 622 | out, found := tree.findCaseInsensitivePath(test.in, true) 623 | if found != test.found || (found && (string(out) != test.out)) { 624 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 625 | test.in, string(out), found, test.out, test.found) 626 | return 627 | } 628 | } 629 | // With fixTrailingSlash = false 630 | for _, test := range tests { 631 | out, found := tree.findCaseInsensitivePath(test.in, false) 632 | if test.slash { 633 | if found { // test needs a trailingSlash fix. It must not be found! 634 | t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) 635 | } 636 | } else { 637 | if found != test.found || (found && (string(out) != test.out)) { 638 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 639 | test.in, string(out), found, test.out, test.found) 640 | return 641 | } 642 | } 643 | } 644 | } 645 | 646 | func TestTreeInvalidNodeType(t *testing.T) { 647 | const panicMsg = "invalid node type" 648 | 649 | tree := &node{} 650 | tree.addRoute("/", fakeHandler("/")) 651 | tree.addRoute("/:page", fakeHandler("/:page")) 652 | 653 | // set invalid node type 654 | tree.children[0].nType = 42 655 | 656 | // normal lookup 657 | recv := catchPanic(func() { 658 | tree.getValue("/test", nil) 659 | }) 660 | if rs, ok := recv.(string); !ok || rs != panicMsg { 661 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 662 | } 663 | 664 | // case-insensitive lookup 665 | recv = catchPanic(func() { 666 | tree.findCaseInsensitivePath("/test", true) 667 | }) 668 | if rs, ok := recv.(string); !ok || rs != panicMsg { 669 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 670 | } 671 | } 672 | --------------------------------------------------------------------------------