├── go.mod ├── unescape_18.go ├── .travis.yml ├── unescape_17.go ├── .gitignore ├── LICENSE ├── path_test.go ├── path.go ├── fallthrough_test.go ├── treemux_16.go ├── group_test.go ├── treemux_17.go ├── panichandler.go ├── context.go ├── group.go ├── tree.go ├── router.go ├── tree_test.go ├── context_test.go ├── README.md └── router_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dimfeld/httptreemux/v5 2 | 3 | go 1.9 4 | -------------------------------------------------------------------------------- /unescape_18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.8 2 | // +build go1.8 3 | 4 | package httptreemux 5 | 6 | import "net/url" 7 | 8 | func unescape(path string) (string, error) { 9 | return url.PathUnescape(path) 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | gobuild_args: "-v -race" 4 | go: 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - 1.8 9 | - 1.9 10 | - tip 11 | 12 | matrix: 13 | allow_failures: 14 | - go: tip 15 | -------------------------------------------------------------------------------- /unescape_17.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.8 2 | // +build !go1.8 3 | 4 | package httptreemux 5 | 6 | import "net/url" 7 | 8 | func unescape(path string) (string, error) { 9 | return url.QueryUnescape(path) 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | 25 | .idea/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014,2015 Daniel Imfeld 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 httptreemux 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 := Clean(test.path); s != test.result { 69 | t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result) 70 | } 71 | if s := Clean(test.result); s != test.result { 72 | t.Errorf("Clean(%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() { Clean(test.result) }) 88 | if allocs > 0 { 89 | t.Errorf("Clean(%q): %v allocs, want zero", test.result, allocs) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 httptreemux 7 | 8 | // Clean 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 Clean(p string) string { 22 | if p == "" { 23 | return "/" 24 | } 25 | 26 | n := len(p) 27 | var buf []byte 28 | 29 | // Invariants: 30 | // reading from path; r is index of next byte to process. 31 | // writing to buf; w is index of next byte to write. 32 | 33 | // path must start with '/' 34 | r := 1 35 | w := 1 36 | 37 | if p[0] != '/' { 38 | r = 0 39 | buf = make([]byte, n+1) 40 | buf[0] = '/' 41 | } 42 | 43 | trailing := n > 2 && p[n-1] == '/' 44 | 45 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 46 | // gets completely inlined (bufApp). So in contrast to the path package this 47 | // loop has no expensive function calls (except 1x make) 48 | 49 | for r < n { 50 | switch { 51 | case p[r] == '/': 52 | // empty path element, trailing slash is added after the end 53 | r++ 54 | 55 | case p[r] == '.' && r+1 == n: 56 | trailing = true 57 | r++ 58 | 59 | case p[r] == '.' && p[r+1] == '/': 60 | // . element 61 | r++ 62 | 63 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 64 | // .. element: remove to last / 65 | r += 2 66 | 67 | if w > 1 { 68 | // can backtrack 69 | w-- 70 | 71 | if buf == nil { 72 | for w > 1 && p[w] != '/' { 73 | w-- 74 | } 75 | } else { 76 | for w > 1 && buf[w] != '/' { 77 | w-- 78 | } 79 | } 80 | } 81 | 82 | default: 83 | // real path element. 84 | // add slash if needed 85 | if w > 1 { 86 | bufApp(&buf, p, w, '/') 87 | w++ 88 | } 89 | 90 | // copy element 91 | for r < n && p[r] != '/' { 92 | bufApp(&buf, p, w, p[r]) 93 | w++ 94 | r++ 95 | } 96 | } 97 | } 98 | 99 | // re-append trailing slash 100 | if trailing && w > 1 { 101 | bufApp(&buf, p, w, '/') 102 | w++ 103 | } 104 | 105 | // Turn empty string into "/" 106 | if w == 0 { 107 | return "/" 108 | } 109 | 110 | if buf == nil { 111 | return p[:w] 112 | } 113 | return string(buf[:w]) 114 | } 115 | 116 | // internal helper to lazily create a buffer if necessary 117 | func bufApp(buf *[]byte, s string, w int, c byte) { 118 | if *buf == nil { 119 | if s[w] == c { 120 | return 121 | } 122 | 123 | *buf = make([]byte, len(s)) 124 | copy(*buf, s[:w]) 125 | } 126 | (*buf)[w] = c 127 | } 128 | -------------------------------------------------------------------------------- /fallthrough_test.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | // When we find a node with a matching path but no handler for a method, 11 | // we should fall through and continue searching the tree for a less specific 12 | // match, i.e. a wildcard or catchall, that does have a handler for that method. 13 | func TestMethodNotAllowedFallthrough(t *testing.T) { 14 | var matchedMethod string 15 | var matchedPath string 16 | var matchedParams map[string]string 17 | 18 | router := New() 19 | 20 | addRoute := func(method, path string) { 21 | router.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params map[string]string) { 22 | matchedMethod = method 23 | matchedPath = path 24 | matchedParams = params 25 | }) 26 | } 27 | 28 | checkRoute := func(method, path, expectedMethod, expectedPath string, 29 | expectedCode int, expectedParams map[string]string) { 30 | 31 | matchedMethod = "" 32 | matchedPath = "" 33 | matchedParams = nil 34 | 35 | w := httptest.NewRecorder() 36 | r, _ := http.NewRequest(method, path, nil) 37 | router.ServeHTTP(w, r) 38 | if expectedCode != w.Code { 39 | t.Errorf("%s %s expected code %d, saw %d", method, path, expectedCode, w.Code) 40 | } 41 | 42 | if w.Code == 200 { 43 | if matchedMethod != method || matchedPath != expectedPath { 44 | t.Errorf("%s %s expected %s %s, saw %s %s", method, path, 45 | expectedMethod, expectedPath, matchedMethod, matchedPath) 46 | } 47 | 48 | if !reflect.DeepEqual(matchedParams, expectedParams) { 49 | t.Errorf("%s %s expected params %+v, saw %+v", method, path, expectedParams, matchedParams) 50 | } 51 | } 52 | } 53 | 54 | addRoute("GET", "/apple/banana/cat") 55 | addRoute("GET", "/apple/potato") 56 | addRoute("POST", "/apple/banana/:abc") 57 | addRoute("POST", "/apple/ban/def") 58 | addRoute("DELETE", "/apple/:seed") 59 | addRoute("DELETE", "/apple/*path") 60 | addRoute("OPTIONS", "/apple/*path") 61 | 62 | checkRoute("GET", "/apple/banana/cat", "GET", "/apple/banana/cat", 200, nil) 63 | checkRoute("POST", "/apple/banana/cat", "POST", "/apple/banana/:abc", 200, 64 | map[string]string{"abc": "cat"}) 65 | checkRoute("POST", "/apple/banana/dog", "POST", "/apple/banana/:abc", 200, 66 | map[string]string{"abc": "dog"}) 67 | 68 | // Wildcards should be checked before catchalls 69 | checkRoute("DELETE", "/apple/banana", "DELETE", "/apple/:seed", 200, 70 | map[string]string{"seed": "banana"}) 71 | checkRoute("DELETE", "/apple/banana/cat", "DELETE", "/apple/*path", 200, 72 | map[string]string{"path": "banana/cat"}) 73 | 74 | checkRoute("POST", "/apple/ban/def", "POST", "/apple/ban/def", 200, nil) 75 | checkRoute("OPTIONS", "/apple/ban/def", "OPTIONS", "/apple/*path", 200, 76 | map[string]string{"path": "ban/def"}) 77 | checkRoute("GET", "/apple/ban/def", "", "", 405, nil) 78 | 79 | // Always fallback to the matching handler no matter how many other 80 | // nodes without proper handlers are found on the way. 81 | checkRoute("OPTIONS", "/apple/banana/cat", "OPTIONS", "/apple/*path", 200, 82 | map[string]string{"path": "banana/cat"}) 83 | checkRoute("OPTIONS", "/apple/bbbb", "OPTIONS", "/apple/*path", 200, 84 | map[string]string{"path": "bbbb"}) 85 | 86 | // Nothing matches on patch 87 | checkRoute("PATCH", "/apple/banana/cat", "", "", 405, nil) 88 | checkRoute("PATCH", "/apple/potato", "", "", 405, nil) 89 | 90 | // And some 404 tests for good measure 91 | checkRoute("GET", "/abc", "", "", 404, nil) 92 | checkRoute("OPTIONS", "/apple", "", "", 404, nil) 93 | } 94 | -------------------------------------------------------------------------------- /treemux_16.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.7 2 | // +build !go1.7 3 | 4 | package httptreemux 5 | 6 | import ( 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | type TreeMux struct { 12 | root *node 13 | mutex sync.RWMutex 14 | 15 | Group 16 | 17 | // The default PanicHandler just returns a 500 code. 18 | PanicHandler PanicHandler 19 | 20 | // The default NotFoundHandler is http.NotFound. 21 | NotFoundHandler func(w http.ResponseWriter, r *http.Request) 22 | 23 | // Any OPTIONS request that matches a path without its own OPTIONS handler will use this handler, 24 | // if set, instead of calling MethodNotAllowedHandler. 25 | OptionsHandler HandlerFunc 26 | 27 | // MethodNotAllowedHandler is called when a pattern matches, but that 28 | // pattern does not have a handler for the requested method. The default 29 | // handler just writes the status code http.StatusMethodNotAllowed and adds 30 | // the required Allowed header. 31 | // The methods parameter contains the map of each method to the corresponding 32 | // handler function. 33 | MethodNotAllowedHandler func(w http.ResponseWriter, r *http.Request, 34 | methods map[string]HandlerFunc) 35 | 36 | // HeadCanUseGet allows the router to use the GET handler to respond to 37 | // HEAD requests if no explicit HEAD handler has been added for the 38 | // matching pattern. This is true by default. 39 | HeadCanUseGet bool 40 | 41 | // RedirectCleanPath allows the router to try clean the current request path, 42 | // if no handler is registered for it, using CleanPath from github.com/dimfeld/httppath. 43 | // This is true by default. 44 | RedirectCleanPath bool 45 | 46 | // RedirectTrailingSlash enables automatic redirection in case router doesn't find a matching route 47 | // for the current request path but a handler for the path with or without the trailing 48 | // slash exists. This is true by default. 49 | RedirectTrailingSlash bool 50 | 51 | // RemoveCatchAllTrailingSlash removes the trailing slash when a catch-all pattern 52 | // is matched, if set to true. By default, catch-all paths are never redirected. 53 | RemoveCatchAllTrailingSlash bool 54 | 55 | // RedirectBehavior sets the default redirect behavior when RedirectTrailingSlash or 56 | // RedirectCleanPath are true. The default value is Redirect301. 57 | RedirectBehavior RedirectBehavior 58 | 59 | // RedirectMethodBehavior overrides the default behavior for a particular HTTP method. 60 | // The key is the method name, and the value is the behavior to use for that method. 61 | RedirectMethodBehavior map[string]RedirectBehavior 62 | 63 | // PathSource determines from where the router gets its path to search. 64 | // By default it pulls the data from the RequestURI member, but this can 65 | // be overridden to use URL.Path instead. 66 | // 67 | // There is a small tradeoff here. Using RequestURI allows the router to handle 68 | // encoded slashes (i.e. %2f) in the URL properly, while URL.Path provides 69 | // better compatibility with some utility functions in the http 70 | // library that modify the Request before passing it to the router. 71 | PathSource PathSource 72 | 73 | // EscapeAddedRoutes controls URI escaping behavior when adding a route to the tree. 74 | // If set to true, the router will add both the route as originally passed, and 75 | // a version passed through URL.EscapedPath. This behavior is disabled by default. 76 | EscapeAddedRoutes bool 77 | 78 | // SafeAddRoutesWhileRunning tells the router to protect all accesses to the tree with an RWMutex. This is only needed 79 | // if you are going to add routes after the router has already begun serving requests. There is a potential 80 | // performance penalty at high load. 81 | SafeAddRoutesWhileRunning bool 82 | } 83 | 84 | func (t *TreeMux) setDefaultRequestContext(r *http.Request) *http.Request { 85 | // Nothing to do on Go 1.6 and before 86 | return r 87 | } 88 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestEmptyGroupAndMapping(t *testing.T) { 10 | defer func() { 11 | if err := recover(); err != nil { 12 | //everything is good, it paniced 13 | } else { 14 | t.Error(`Expected NewGroup("")`) 15 | } 16 | }() 17 | New().GET("", func(w http.ResponseWriter, _ *http.Request, _ map[string]string) {}) 18 | } 19 | func TestSubGroupSlashMapping(t *testing.T) { 20 | r := New() 21 | r.NewGroup("/foo").GET("/", func(w http.ResponseWriter, _ *http.Request, _ map[string]string) { 22 | w.WriteHeader(200) 23 | }) 24 | 25 | var req *http.Request 26 | var recorder *httptest.ResponseRecorder 27 | 28 | req, _ = http.NewRequest("GET", "/foo", nil) 29 | recorder = httptest.NewRecorder() 30 | r.ServeHTTP(recorder, req) 31 | if recorder.Code != 301 { //should get redirected 32 | t.Error(`/foo on NewGroup("/foo").GET("/") should result in 301 response, got:`, recorder.Code) 33 | } 34 | 35 | req, _ = http.NewRequest("GET", "/foo/", nil) 36 | recorder = httptest.NewRecorder() 37 | r.ServeHTTP(recorder, req) 38 | if recorder.Code != 200 { 39 | t.Error(`/foo/ on NewGroup("/foo").GET("/"") should result in 200 response, got:`, recorder.Code) 40 | } 41 | } 42 | 43 | func TestSubGroupEmptyMapping(t *testing.T) { 44 | r := New() 45 | r.NewGroup("/foo").GET("", func(w http.ResponseWriter, _ *http.Request, _ map[string]string) { 46 | w.WriteHeader(200) 47 | }) 48 | req, _ := http.NewRequest("GET", "/foo", nil) 49 | recorder := httptest.NewRecorder() 50 | r.ServeHTTP(recorder, req) 51 | if recorder.Code != 200 { 52 | t.Error(`/foo on NewGroup("/foo").GET("") should result in 200 response, got:`, recorder.Code) 53 | } 54 | } 55 | 56 | func TestGroupCaseInsensitiveRouting(t *testing.T) { 57 | r := New() 58 | r.CaseInsensitive = true 59 | r.NewGroup("/MY-path").GET("", func(w http.ResponseWriter, _ *http.Request, _ map[string]string) { 60 | w.WriteHeader(200) 61 | }) 62 | 63 | req, _ := http.NewRequest("GET", "/MY-PATH", nil) 64 | recorder := httptest.NewRecorder() 65 | r.ServeHTTP(recorder, req) 66 | if recorder.Code != http.StatusOK { 67 | t.Errorf("expected 200 response for case-insensitive request. Received: %d", recorder.Code) 68 | } 69 | } 70 | 71 | func TestGroupMethods(t *testing.T) { 72 | for _, scenario := range scenarios { 73 | t.Log(scenario.description) 74 | testGroupMethods(t, scenario.RequestCreator, false) 75 | testGroupMethods(t, scenario.RequestCreator, true) 76 | } 77 | } 78 | 79 | func TestInvalidHandle(t *testing.T) { 80 | defer func() { 81 | if err := recover(); err == nil { 82 | t.Error("Bad handle path should have caused a panic") 83 | } 84 | }() 85 | New().NewGroup("/foo").GET("bar", nil) 86 | } 87 | 88 | func TestInvalidSubPath(t *testing.T) { 89 | defer func() { 90 | if err := recover(); err == nil { 91 | t.Error("Bad sub-path should have caused a panic") 92 | } 93 | }() 94 | New().NewGroup("/foo").NewGroup("bar") 95 | } 96 | 97 | func TestInvalidPath(t *testing.T) { 98 | defer func() { 99 | if err := recover(); err == nil { 100 | t.Error("Bad path should have caused a panic") 101 | } 102 | }() 103 | New().NewGroup("foo") 104 | } 105 | 106 | // Liberally borrowed from router_test 107 | func testGroupMethods(t *testing.T, reqGen RequestCreator, headCanUseGet bool) { 108 | var result string 109 | makeHandler := func(method string) HandlerFunc { 110 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 111 | result = method 112 | } 113 | } 114 | router := New() 115 | router.HeadCanUseGet = headCanUseGet 116 | // Testing with a sub-group of a group as that will test everything at once 117 | g := router.NewGroup("/base").NewGroup("/user") 118 | g.GET("/:param", makeHandler("GET")) 119 | g.POST("/:param", makeHandler("POST")) 120 | g.PATCH("/:param", makeHandler("PATCH")) 121 | g.PUT("/:param", makeHandler("PUT")) 122 | g.DELETE("/:param", makeHandler("DELETE")) 123 | 124 | testMethod := func(method, expect string) { 125 | result = "" 126 | w := httptest.NewRecorder() 127 | r, _ := reqGen(method, "/base/user/"+method, nil) 128 | router.ServeHTTP(w, r) 129 | if expect == "" && w.Code != http.StatusMethodNotAllowed { 130 | t.Errorf("Method %s not expected to match but saw code %d", method, w.Code) 131 | } 132 | 133 | if result != expect { 134 | t.Errorf("Method %s got result %s", method, result) 135 | } 136 | } 137 | 138 | testMethod("GET", "GET") 139 | testMethod("POST", "POST") 140 | testMethod("PATCH", "PATCH") 141 | testMethod("PUT", "PUT") 142 | testMethod("DELETE", "DELETE") 143 | if headCanUseGet { 144 | t.Log("Test implicit HEAD with HeadCanUseGet = true") 145 | testMethod("HEAD", "GET") 146 | } else { 147 | t.Log("Test implicit HEAD with HeadCanUseGet = false") 148 | testMethod("HEAD", "") 149 | } 150 | 151 | router.HEAD("/base/user/:param", makeHandler("HEAD")) 152 | testMethod("HEAD", "HEAD") 153 | } 154 | 155 | // Ensure that setting a GET handler doesn't overwrite an explciit HEAD handler. 156 | func TestSetGetAfterHead(t *testing.T) { 157 | var result string 158 | makeHandler := func(method string) HandlerFunc { 159 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 160 | result = method 161 | } 162 | } 163 | 164 | router := New() 165 | router.HeadCanUseGet = true 166 | router.HEAD("/abc", makeHandler("HEAD")) 167 | router.GET("/abc", makeHandler("GET")) 168 | 169 | testMethod := func(method, expect string) { 170 | result = "" 171 | w := httptest.NewRecorder() 172 | r, _ := http.NewRequest(method, "/abc", nil) 173 | router.ServeHTTP(w, r) 174 | 175 | if result != expect { 176 | t.Errorf("Method %s got result %s", method, result) 177 | } 178 | } 179 | 180 | testMethod("HEAD", "HEAD") 181 | testMethod("GET", "GET") 182 | } 183 | -------------------------------------------------------------------------------- /treemux_17.go: -------------------------------------------------------------------------------- 1 | //go:build go1.7 2 | // +build go1.7 3 | 4 | package httptreemux 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | type TreeMux struct { 13 | root *node 14 | mutex sync.RWMutex 15 | 16 | Group 17 | 18 | // The default PanicHandler just returns a 500 code. 19 | PanicHandler PanicHandler 20 | 21 | // The default NotFoundHandler is http.NotFound. 22 | NotFoundHandler func(w http.ResponseWriter, r *http.Request) 23 | 24 | // Any OPTIONS request that matches a path without its own OPTIONS handler will use this handler, 25 | // if set, instead of calling MethodNotAllowedHandler. 26 | OptionsHandler HandlerFunc 27 | 28 | // MethodNotAllowedHandler is called when a pattern matches, but that 29 | // pattern does not have a handler for the requested method. The default 30 | // handler just writes the status code http.StatusMethodNotAllowed and adds 31 | // the required Allowed header. 32 | // The methods parameter contains the map of each method to the corresponding 33 | // handler function. 34 | MethodNotAllowedHandler func(w http.ResponseWriter, r *http.Request, 35 | methods map[string]HandlerFunc) 36 | 37 | // HeadCanUseGet allows the router to use the GET handler to respond to 38 | // HEAD requests if no explicit HEAD handler has been added for the 39 | // matching pattern. This is true by default. 40 | HeadCanUseGet bool 41 | 42 | // RedirectCleanPath allows the router to try clean the current request path, 43 | // if no handler is registered for it, using CleanPath from github.com/dimfeld/httppath. 44 | // This is true by default. 45 | RedirectCleanPath bool 46 | 47 | // RedirectTrailingSlash enables automatic redirection in case router doesn't find a matching route 48 | // for the current request path but a handler for the path with or without the trailing 49 | // slash exists. This is true by default. 50 | RedirectTrailingSlash bool 51 | 52 | // RemoveCatchAllTrailingSlash removes the trailing slash when a catch-all pattern 53 | // is matched, if set to true. By default, catch-all paths are never redirected. 54 | RemoveCatchAllTrailingSlash bool 55 | 56 | // RedirectBehavior sets the default redirect behavior when RedirectTrailingSlash or 57 | // RedirectCleanPath are true. The default value is Redirect301. 58 | RedirectBehavior RedirectBehavior 59 | 60 | // RedirectMethodBehavior overrides the default behavior for a particular HTTP method. 61 | // The key is the method name, and the value is the behavior to use for that method. 62 | RedirectMethodBehavior map[string]RedirectBehavior 63 | 64 | // PathSource determines from where the router gets its path to search. 65 | // By default it pulls the data from the RequestURI member, but this can 66 | // be overridden to use URL.Path instead. 67 | // 68 | // There is a small tradeoff here. Using RequestURI allows the router to handle 69 | // encoded slashes (i.e. %2f) in the URL properly, while URL.Path provides 70 | // better compatibility with some utility functions in the http 71 | // library that modify the Request before passing it to the router. 72 | PathSource PathSource 73 | 74 | // EscapeAddedRoutes controls URI escaping behavior when adding a route to the tree. 75 | // If set to true, the router will add both the route as originally passed, and 76 | // a version passed through URL.EscapedPath. This behavior is disabled by default. 77 | EscapeAddedRoutes bool 78 | 79 | // If present, override the default context with this one. 80 | DefaultContext context.Context 81 | 82 | // SafeAddRoutesWhileRunning tells the router to protect all accesses to the tree with an RWMutex. This is only needed 83 | // if you are going to add routes after the router has already begun serving requests. There is a potential 84 | // performance penalty at high load. 85 | SafeAddRoutesWhileRunning bool 86 | 87 | // CaseInsensitive determines if routes should be treated as case-insensitive. 88 | CaseInsensitive bool 89 | } 90 | 91 | func (t *TreeMux) setDefaultRequestContext(r *http.Request) *http.Request { 92 | if t.DefaultContext != nil { 93 | r = r.WithContext(t.DefaultContext) 94 | } 95 | 96 | return r 97 | } 98 | 99 | type ContextMux struct { 100 | *TreeMux 101 | *ContextGroup 102 | } 103 | 104 | // NewContextMux returns a TreeMux preconfigured to work with standard http 105 | // Handler functions and context objects. 106 | func NewContextMux() *ContextMux { 107 | mux := New() 108 | cg := mux.UsingContext() 109 | 110 | return &ContextMux{ 111 | TreeMux: mux, 112 | ContextGroup: cg, 113 | } 114 | } 115 | 116 | func (cm *ContextMux) NewGroup(path string) *ContextGroup { 117 | return cm.ContextGroup.NewGroup(path) 118 | } 119 | 120 | // GET is convenience method for handling GET requests on a context group. 121 | func (cm *ContextMux) GET(path string, handler http.HandlerFunc) { 122 | cm.ContextGroup.Handle("GET", path, handler) 123 | } 124 | 125 | // POST is convenience method for handling POST requests on a context group. 126 | func (cm *ContextMux) POST(path string, handler http.HandlerFunc) { 127 | cm.ContextGroup.Handle("POST", path, handler) 128 | } 129 | 130 | // PUT is convenience method for handling PUT requests on a context group. 131 | func (cm *ContextMux) PUT(path string, handler http.HandlerFunc) { 132 | cm.ContextGroup.Handle("PUT", path, handler) 133 | } 134 | 135 | // DELETE is convenience method for handling DELETE requests on a context group. 136 | func (cm *ContextMux) DELETE(path string, handler http.HandlerFunc) { 137 | cm.ContextGroup.Handle("DELETE", path, handler) 138 | } 139 | 140 | // PATCH is convenience method for handling PATCH requests on a context group. 141 | func (cm *ContextMux) PATCH(path string, handler http.HandlerFunc) { 142 | cm.ContextGroup.Handle("PATCH", path, handler) 143 | } 144 | 145 | // HEAD is convenience method for handling HEAD requests on a context group. 146 | func (cm *ContextMux) HEAD(path string, handler http.HandlerFunc) { 147 | cm.ContextGroup.Handle("HEAD", path, handler) 148 | } 149 | 150 | // OPTIONS is convenience method for handling OPTIONS requests on a context group. 151 | func (cm *ContextMux) OPTIONS(path string, handler http.HandlerFunc) { 152 | cm.ContextGroup.Handle("OPTIONS", path, handler) 153 | } 154 | -------------------------------------------------------------------------------- /panichandler.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "html/template" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strings" 11 | ) 12 | 13 | // SimplePanicHandler just returns error 500. 14 | func SimplePanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) { 15 | w.WriteHeader(http.StatusInternalServerError) 16 | } 17 | 18 | // ShowErrorsPanicHandler prints a nice representation of an error to the browser. 19 | // This was taken from github.com/gocraft/web, which adapted it from the Traffic project. 20 | func ShowErrorsPanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) { 21 | const size = 4096 22 | stack := make([]byte, size) 23 | stack = stack[:runtime.Stack(stack, false)] 24 | renderPrettyError(w, r, err, stack) 25 | } 26 | 27 | func makeErrorData(r *http.Request, err interface{}, stack []byte, filePath string, line int) map[string]interface{} { 28 | 29 | data := map[string]interface{}{ 30 | "Stack": string(stack), 31 | "Params": r.URL.Query(), 32 | "Method": r.Method, 33 | "FilePath": filePath, 34 | "Line": line, 35 | "Lines": readErrorFileLines(filePath, line), 36 | } 37 | 38 | if e, ok := err.(error); ok { 39 | data["Error"] = e.Error() 40 | } else { 41 | data["Error"] = err 42 | } 43 | 44 | return data 45 | } 46 | 47 | func renderPrettyError(rw http.ResponseWriter, req *http.Request, err interface{}, stack []byte) { 48 | _, filePath, line, _ := runtime.Caller(5) 49 | 50 | data := makeErrorData(req, err, stack, filePath, line) 51 | rw.Header().Set("Content-Type", "text/html") 52 | rw.WriteHeader(http.StatusInternalServerError) 53 | 54 | tpl := template.Must(template.New("ErrorPage").Parse(panicPageTpl)) 55 | tpl.Execute(rw, data) 56 | } 57 | 58 | func ShowErrorsJsonPanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) { 59 | const size = 4096 60 | stack := make([]byte, size) 61 | stack = stack[:runtime.Stack(stack, false)] 62 | 63 | _, filePath, line, _ := runtime.Caller(4) 64 | data := makeErrorData(r, err, stack, filePath, line) 65 | 66 | w.Header().Set("Content-Type", "application/json") 67 | w.WriteHeader(http.StatusInternalServerError) 68 | json.NewEncoder(w).Encode(data) 69 | } 70 | 71 | func readErrorFileLines(filePath string, errorLine int) map[int]string { 72 | lines := make(map[int]string) 73 | 74 | file, err := os.Open(filePath) 75 | if err != nil { 76 | return lines 77 | } 78 | 79 | defer file.Close() 80 | 81 | reader := bufio.NewReader(file) 82 | currentLine := 0 83 | for { 84 | line, err := reader.ReadString('\n') 85 | if err != nil || currentLine > errorLine+5 { 86 | break 87 | } 88 | 89 | currentLine++ 90 | 91 | if currentLine >= errorLine-5 { 92 | lines[currentLine] = strings.Replace(line, "\n", "", -1) 93 | } 94 | } 95 | 96 | return lines 97 | } 98 | 99 | const panicPageTpl string = ` 100 | 101 | 102 | Panic 103 | 104 | 171 | 172 | 173 |
174 |
175 |

Error

176 |
177 |
178 | 179 |
180 |

{{ .Error }}

181 |
182 | 183 |
184 |

185 | In {{ .FilePath }}:{{ .Line }}

186 |

187 | 188 | 189 | 190 | 193 | 196 | 197 |
191 |
{{ range $lineNumber, $line :=  .Lines }}{{ $lineNumber }}{{ end }}
192 |
194 |
{{ range $lineNumber, $line :=  .Lines }}{{ $line }}
{{ end }}
195 |
198 |

Stack

199 |
{{ .Stack }}
200 |

Request

201 |

Method: {{ .Method }}

202 |

Parameters:

203 | 208 |
209 | 210 | 211 | ` 212 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | //go:build go1.7 2 | // +build go1.7 3 | 4 | package httptreemux 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | ) 10 | 11 | // ContextGroup is a wrapper around Group, with the purpose of mimicking its API, but with the use of http.HandlerFunc-based handlers. 12 | // Instead of passing a parameter map via the handler (i.e. httptreemux.HandlerFunc), the path parameters are accessed via the request 13 | // object's context. 14 | type ContextGroup struct { 15 | group *Group 16 | } 17 | 18 | // Use appends a middleware handler to the Group middleware stack. 19 | func (cg *ContextGroup) Use(fn MiddlewareFunc) { 20 | cg.group.Use(fn) 21 | } 22 | 23 | // UseHandler is like Use but accepts http.Handler middleware. 24 | func (cg *ContextGroup) UseHandler(middleware func(http.Handler) http.Handler) { 25 | cg.group.UseHandler(middleware) 26 | } 27 | 28 | // UsingContext wraps the receiver to return a new instance of a ContextGroup. 29 | // The returned ContextGroup is a sibling to its wrapped Group, within the parent TreeMux. 30 | // The choice of using a *Group as the receiver, as opposed to a function parameter, allows chaining 31 | // while method calls between a TreeMux, Group, and ContextGroup. For example: 32 | // 33 | // tree := httptreemux.New() 34 | // group := tree.NewGroup("/api") 35 | // 36 | // group.GET("/v1", func(w http.ResponseWriter, r *http.Request, params map[string]string) { 37 | // w.Write([]byte(`GET /api/v1`)) 38 | // }) 39 | // 40 | // group.UsingContext().GET("/v2", func(w http.ResponseWriter, r *http.Request) { 41 | // w.Write([]byte(`GET /api/v2`)) 42 | // }) 43 | // 44 | // http.ListenAndServe(":8080", tree) 45 | func (g *Group) UsingContext() *ContextGroup { 46 | return &ContextGroup{g} 47 | } 48 | 49 | // NewContextGroup adds a child context group to its path. 50 | func (cg *ContextGroup) NewContextGroup(path string) *ContextGroup { 51 | return &ContextGroup{cg.group.NewGroup(path)} 52 | } 53 | 54 | func (cg *ContextGroup) NewGroup(path string) *ContextGroup { 55 | return cg.NewContextGroup(path) 56 | } 57 | 58 | func (cg *ContextGroup) wrapHandler(path string, handler HandlerFunc) HandlerFunc { 59 | if len(cg.group.stack) > 0 { 60 | handler = handlerWithMiddlewares(handler, cg.group.stack) 61 | } 62 | 63 | // add the context data after adding all middleware 64 | fullPath := cg.group.path + path 65 | return func(writer http.ResponseWriter, request *http.Request, m map[string]string) { 66 | routeData := &contextData{ 67 | route: fullPath, 68 | params: m, 69 | } 70 | request = request.WithContext(AddRouteDataToContext(request.Context(), routeData)) 71 | handler(writer, request, m) 72 | } 73 | } 74 | 75 | // Handle allows handling HTTP requests via an http.HandlerFunc, as opposed to an httptreemux.HandlerFunc. 76 | // Any parameters from the request URL are stored in a map[string]string in the request's context. 77 | func (cg *ContextGroup) Handle(method, path string, handler http.HandlerFunc) { 78 | cg.group.mux.mutex.Lock() 79 | defer cg.group.mux.mutex.Unlock() 80 | 81 | wrapped := cg.wrapHandler(path, func(w http.ResponseWriter, r *http.Request, params map[string]string) { 82 | handler(w, r) 83 | }) 84 | 85 | cg.group.addFullStackHandler(method, path, wrapped) 86 | } 87 | 88 | // Handler allows handling HTTP requests via an http.Handler interface, as opposed to an httptreemux.HandlerFunc. 89 | // Any parameters from the request URL are stored in a map[string]string in the request's context. 90 | func (cg *ContextGroup) Handler(method, path string, handler http.Handler) { 91 | cg.group.mux.mutex.Lock() 92 | defer cg.group.mux.mutex.Unlock() 93 | 94 | wrapped := cg.wrapHandler(path, func(w http.ResponseWriter, r *http.Request, params map[string]string) { 95 | handler.ServeHTTP(w, r) 96 | }) 97 | 98 | cg.group.addFullStackHandler(method, path, wrapped) 99 | } 100 | 101 | // GET is convenience method for handling GET requests on a context group. 102 | func (cg *ContextGroup) GET(path string, handler http.HandlerFunc) { 103 | cg.Handle("GET", path, handler) 104 | } 105 | 106 | // POST is convenience method for handling POST requests on a context group. 107 | func (cg *ContextGroup) POST(path string, handler http.HandlerFunc) { 108 | cg.Handle("POST", path, handler) 109 | } 110 | 111 | // PUT is convenience method for handling PUT requests on a context group. 112 | func (cg *ContextGroup) PUT(path string, handler http.HandlerFunc) { 113 | cg.Handle("PUT", path, handler) 114 | } 115 | 116 | // DELETE is convenience method for handling DELETE requests on a context group. 117 | func (cg *ContextGroup) DELETE(path string, handler http.HandlerFunc) { 118 | cg.Handle("DELETE", path, handler) 119 | } 120 | 121 | // PATCH is convenience method for handling PATCH requests on a context group. 122 | func (cg *ContextGroup) PATCH(path string, handler http.HandlerFunc) { 123 | cg.Handle("PATCH", path, handler) 124 | } 125 | 126 | // HEAD is convenience method for handling HEAD requests on a context group. 127 | func (cg *ContextGroup) HEAD(path string, handler http.HandlerFunc) { 128 | cg.Handle("HEAD", path, handler) 129 | } 130 | 131 | // OPTIONS is convenience method for handling OPTIONS requests on a context group. 132 | func (cg *ContextGroup) OPTIONS(path string, handler http.HandlerFunc) { 133 | cg.Handle("OPTIONS", path, handler) 134 | } 135 | 136 | type contextData struct { 137 | route string 138 | params map[string]string 139 | } 140 | 141 | func (cd *contextData) Route() string { 142 | return cd.route 143 | } 144 | 145 | func (cd *contextData) Params() map[string]string { 146 | if cd.params != nil { 147 | return cd.params 148 | } 149 | return map[string]string{} 150 | } 151 | 152 | // ContextRouteData is the information associated with the matched path. 153 | // Route() returns the matched route, without expanded wildcards. 154 | // Params() returns a map of the route's wildcards and their matched values. 155 | type ContextRouteData interface { 156 | Route() string 157 | Params() map[string]string 158 | } 159 | 160 | // ContextParams returns a map of the route's wildcards and their matched values. 161 | func ContextParams(ctx context.Context) map[string]string { 162 | if cd := ContextData(ctx); cd != nil { 163 | return cd.Params() 164 | } 165 | return map[string]string{} 166 | } 167 | 168 | // ContextRoute returns the matched route, without expanded wildcards. 169 | func ContextRoute(ctx context.Context) string { 170 | if cd := ContextData(ctx); cd != nil { 171 | return cd.Route() 172 | } 173 | return "" 174 | } 175 | 176 | // ContextData returns the ContextRouteData associated with the matched path 177 | func ContextData(ctx context.Context) ContextRouteData { 178 | if p, ok := ctx.Value(contextDataKey).(ContextRouteData); ok { 179 | return p 180 | } 181 | return nil 182 | } 183 | 184 | // AddRouteDataToContext can be used for testing handlers, to insert route data into the request's `Context`. 185 | func AddRouteDataToContext(ctx context.Context, data ContextRouteData) context.Context { 186 | return context.WithValue(ctx, contextDataKey, data) 187 | } 188 | 189 | // AddParamsToContext inserts a parameters map into a context using 190 | // the package's internal context key. 191 | func AddParamsToContext(ctx context.Context, params map[string]string) context.Context { 192 | return AddRouteDataToContext(ctx, &contextData{ 193 | params: params, 194 | }) 195 | } 196 | 197 | // AddRouteToContext inserts a route into a context using 198 | // the package's internal context key. 199 | func AddRouteToContext(ctx context.Context, route string) context.Context { 200 | return AddRouteDataToContext(ctx, &contextData{ 201 | route: route, 202 | }) 203 | } 204 | 205 | type contextKey int 206 | 207 | // contextDataKey is used to retrieve the path's params map and matched route 208 | // from a request's context. 209 | const contextDataKey contextKey = 0 210 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | type MiddlewareFunc func(next HandlerFunc) HandlerFunc 11 | 12 | func handlerWithMiddlewares(handler HandlerFunc, stack []MiddlewareFunc) HandlerFunc { 13 | for i := len(stack) - 1; i >= 0; i-- { 14 | handler = stack[i](handler) 15 | } 16 | return handler 17 | } 18 | 19 | type Group struct { 20 | path string 21 | mux *TreeMux 22 | stack []MiddlewareFunc 23 | } 24 | 25 | // Add a sub-group to this group 26 | func (g *Group) NewGroup(path string) *Group { 27 | if len(path) < 1 { 28 | panic("Group path must not be empty") 29 | } 30 | 31 | checkPath(path) 32 | path = g.path + path 33 | //Don't want trailing slash as all sub-paths start with slash 34 | if path[len(path)-1] == '/' { 35 | path = path[:len(path)-1] 36 | } 37 | return &Group{ 38 | path: path, 39 | mux: g.mux, 40 | stack: g.stack[:len(g.stack):len(g.stack)], 41 | } 42 | } 43 | 44 | // Use appends a middleware handler to the Group middleware stack. 45 | func (g *Group) Use(fn MiddlewareFunc) { 46 | g.stack = append(g.stack, fn) 47 | } 48 | 49 | type handlerWithParams struct { 50 | handler HandlerFunc 51 | params map[string]string 52 | } 53 | 54 | func (h handlerWithParams) ServeHTTP(w http.ResponseWriter, r *http.Request) { 55 | h.handler(w, r, h.params) 56 | } 57 | 58 | // UseHandler is like Use but accepts http.Handler middleware. 59 | func (g *Group) UseHandler(middleware func(http.Handler) http.Handler) { 60 | g.stack = append(g.stack, func(next HandlerFunc) HandlerFunc { 61 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 62 | nextHandler := handlerWithParams{ 63 | handler: next, 64 | params: params, 65 | } 66 | middleware(nextHandler).ServeHTTP(w, r) 67 | } 68 | }) 69 | } 70 | 71 | // Path elements starting with : indicate a wildcard in the path. A wildcard will only match on a 72 | // single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, 73 | // but not `/post/1/2`. 74 | // 75 | // A path element starting with * is a catch-all, whose value will be a string containing all text 76 | // in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a 77 | // requested URL `images/abc/def`, path would contain `abc/def`. 78 | // 79 | // # Routing Rule Priority 80 | // 81 | // The priority rules in the router are simple. 82 | // 83 | // 1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned. 84 | // 85 | // 2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL. 86 | // 87 | // 3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern. 88 | // 89 | // So with the following patterns, we'll see certain matches: 90 | // 91 | // router = httptreemux.New() 92 | // router.GET("/:page", pageHandler) 93 | // router.GET("/:year/:month/:post", postHandler) 94 | // router.GET("/:year/:month", archiveHandler) 95 | // router.GET("/images/*path", staticHandler) 96 | // router.GET("/favicon.ico", staticHandler) 97 | // 98 | // /abc will match /:page 99 | // /2014/05 will match /:year/:month 100 | // /2014/05/really-great-blog-post will match /:year/:month/:post 101 | // /images/CoolImage.gif will match /images/*path 102 | // /images/2014/05/MayImage.jpg will also match /images/*path, with all the text after /images stored in the variable path. 103 | // /favicon.ico will match /favicon.ico 104 | // 105 | // # Trailing Slashes 106 | // 107 | // The router has special handling for paths with trailing slashes. If a pattern is added to the 108 | // router with a trailing slash, any matches on that pattern without a trailing slash will be 109 | // redirected to the version with the slash. If a pattern does not have a trailing slash, matches on 110 | // that pattern with a trailing slash will be redirected to the version without. 111 | // 112 | // The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a 113 | // method with a trailing slash, all other methods for that pattern will also be considered to have a 114 | // trailing slash, regardless of whether or not it is specified for those methods too. 115 | // 116 | // This behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By 117 | // default it is set to true. The specifics of the redirect depend on RedirectBehavior. 118 | // 119 | // One exception to this rule is catch-all patterns. By default, trailing slash redirection is 120 | // disabled on catch-all patterns, since the structure of the entire URL and the desired patterns 121 | // can not be predicted. If trailing slash removal is desired on catch-all patterns, set 122 | // TreeMux.RemoveCatchAllTrailingSlash to true. 123 | // 124 | // router = httptreemux.New() 125 | // router.GET("/about", pageHandler) 126 | // router.GET("/posts/", postIndexHandler) 127 | // router.POST("/posts", postFormHandler) 128 | // 129 | // GET /about will match normally. 130 | // GET /about/ will redirect to /about. 131 | // GET /posts will redirect to /posts/. 132 | // GET /posts/ will match normally. 133 | // POST /posts will redirect to /posts/, because the GET method used a trailing slash. 134 | func (g *Group) Handle(method string, path string, handler HandlerFunc) { 135 | g.mux.mutex.Lock() 136 | defer g.mux.mutex.Unlock() 137 | 138 | if len(g.stack) > 0 { 139 | handler = handlerWithMiddlewares(handler, g.stack) 140 | } 141 | 142 | g.addFullStackHandler(method, path, handler) 143 | } 144 | 145 | func (g *Group) addFullStackHandler(method string, path string, handler HandlerFunc) { 146 | addSlash := false 147 | addOne := func(thePath string) { 148 | if g.mux.CaseInsensitive { 149 | thePath = strings.ToLower(thePath) 150 | } 151 | 152 | node := g.mux.root.addPath(thePath[1:], nil, false) 153 | if addSlash { 154 | node.addSlash = true 155 | } 156 | node.setHandler(method, handler, false) 157 | 158 | if g.mux.HeadCanUseGet && method == "GET" && node.leafHandler["HEAD"] == nil { 159 | node.setHandler("HEAD", handler, true) 160 | } 161 | } 162 | 163 | checkPath(path) 164 | path = g.path + path 165 | if len(path) == 0 { 166 | panic("Cannot map an empty path") 167 | } 168 | 169 | if len(path) > 1 && path[len(path)-1] == '/' && g.mux.RedirectTrailingSlash { 170 | addSlash = true 171 | path = path[:len(path)-1] 172 | } 173 | 174 | if g.mux.EscapeAddedRoutes { 175 | u, err := url.ParseRequestURI(path) 176 | if err != nil { 177 | panic("URL parsing error " + err.Error() + " on url " + path) 178 | } 179 | escapedPath := unescapeSpecial(u.String()) 180 | 181 | if escapedPath != path { 182 | addOne(escapedPath) 183 | } 184 | } 185 | 186 | addOne(path) 187 | 188 | } 189 | 190 | // Syntactic sugar for Handle("GET", path, handler) 191 | func (g *Group) GET(path string, handler HandlerFunc) { 192 | g.Handle("GET", path, handler) 193 | } 194 | 195 | // Syntactic sugar for Handle("POST", path, handler) 196 | func (g *Group) POST(path string, handler HandlerFunc) { 197 | g.Handle("POST", path, handler) 198 | } 199 | 200 | // Syntactic sugar for Handle("PUT", path, handler) 201 | func (g *Group) PUT(path string, handler HandlerFunc) { 202 | g.Handle("PUT", path, handler) 203 | } 204 | 205 | // Syntactic sugar for Handle("DELETE", path, handler) 206 | func (g *Group) DELETE(path string, handler HandlerFunc) { 207 | g.Handle("DELETE", path, handler) 208 | } 209 | 210 | // Syntactic sugar for Handle("PATCH", path, handler) 211 | func (g *Group) PATCH(path string, handler HandlerFunc) { 212 | g.Handle("PATCH", path, handler) 213 | } 214 | 215 | // Syntactic sugar for Handle("HEAD", path, handler) 216 | func (g *Group) HEAD(path string, handler HandlerFunc) { 217 | g.Handle("HEAD", path, handler) 218 | } 219 | 220 | // Syntactic sugar for Handle("OPTIONS", path, handler) 221 | func (g *Group) OPTIONS(path string, handler HandlerFunc) { 222 | g.Handle("OPTIONS", path, handler) 223 | } 224 | 225 | func checkPath(path string) { 226 | // All non-empty paths must start with a slash 227 | if len(path) > 0 && path[0] != '/' { 228 | panic(fmt.Sprintf("Path %s must start with slash", path)) 229 | } 230 | } 231 | 232 | func unescapeSpecial(s string) string { 233 | // Look for sequences of \*, *, and \: that were escaped, and undo some of that escaping. 234 | 235 | // Unescape /* since it references a wildcard token. 236 | s = strings.Replace(s, "/%2A", "/*", -1) 237 | 238 | // Unescape /\: since it references a literal colon 239 | s = strings.Replace(s, "/%5C:", "/\\:", -1) 240 | 241 | // Replace escaped /\\: with /\: 242 | s = strings.Replace(s, "/%5C%5C:", "/%5C:", -1) 243 | 244 | // Replace escaped /\* with /* 245 | s = strings.Replace(s, "/%5C%2A", "/%2A", -1) 246 | 247 | // Replace escaped /\\* with /\* 248 | s = strings.Replace(s, "/%5C%5C%2A", "/%5C%2A", -1) 249 | 250 | return s 251 | } 252 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type node struct { 9 | path string 10 | 11 | priority int 12 | 13 | // The list of static children to check. 14 | staticIndices []byte 15 | staticChild []*node 16 | 17 | // If none of the above match, check the wildcard children 18 | wildcardChild *node 19 | 20 | // If none of the above match, then we use the catch-all, if applicable. 21 | catchAllChild *node 22 | 23 | // Data for the node is below. 24 | 25 | addSlash bool 26 | isCatchAll bool 27 | // If true, the head handler was set implicitly, so let it also be set explicitly. 28 | implicitHead bool 29 | // If this node is the end of the URL, then call the handler, if applicable. 30 | leafHandler map[string]HandlerFunc 31 | 32 | // The names of the parameters to apply. 33 | leafWildcardNames []string 34 | } 35 | 36 | func (n *node) sortStaticChild(i int) { 37 | for i > 0 && n.staticChild[i].priority > n.staticChild[i-1].priority { 38 | n.staticChild[i], n.staticChild[i-1] = n.staticChild[i-1], n.staticChild[i] 39 | n.staticIndices[i], n.staticIndices[i-1] = n.staticIndices[i-1], n.staticIndices[i] 40 | i -= 1 41 | } 42 | } 43 | 44 | func (n *node) setHandler(verb string, handler HandlerFunc, implicitHead bool) { 45 | if n.leafHandler == nil { 46 | n.leafHandler = make(map[string]HandlerFunc) 47 | } 48 | _, ok := n.leafHandler[verb] 49 | if ok && (verb != "HEAD" || !n.implicitHead) { 50 | panic(fmt.Sprintf("%s already handles %s", n.path, verb)) 51 | } 52 | n.leafHandler[verb] = handler 53 | 54 | if verb == "HEAD" { 55 | n.implicitHead = implicitHead 56 | } 57 | } 58 | 59 | func (n *node) addPath(path string, wildcards []string, inStaticToken bool) *node { 60 | leaf := len(path) == 0 61 | if leaf { 62 | if wildcards != nil { 63 | // Make sure the current wildcards are the same as the old ones. 64 | // If not then we have an ambiguous path. 65 | if n.leafWildcardNames != nil { 66 | if len(n.leafWildcardNames) != len(wildcards) { 67 | // This should never happen. 68 | panic("Reached leaf node with differing wildcard array length. Please report this as a bug.") 69 | } 70 | 71 | for i := 0; i < len(wildcards); i++ { 72 | if n.leafWildcardNames[i] != wildcards[i] { 73 | panic(fmt.Sprintf("Wildcards %v are ambiguous with wildcards %v", 74 | n.leafWildcardNames, wildcards)) 75 | } 76 | } 77 | } else { 78 | // No wildcards yet, so just add the existing set. 79 | n.leafWildcardNames = wildcards 80 | } 81 | } 82 | 83 | return n 84 | } 85 | 86 | c := path[0] 87 | nextSlash := strings.Index(path, "/") 88 | var thisToken string 89 | var tokenEnd int 90 | 91 | if c == '/' { 92 | // Done processing the previous token, so reset inStaticToken to false. 93 | thisToken = "/" 94 | tokenEnd = 1 95 | } else if nextSlash == -1 { 96 | thisToken = path 97 | tokenEnd = len(path) 98 | } else { 99 | thisToken = path[0:nextSlash] 100 | tokenEnd = nextSlash 101 | } 102 | remainingPath := path[tokenEnd:] 103 | 104 | if c == '*' && !inStaticToken { 105 | // Token starts with a *, so it's a catch-all 106 | thisToken = thisToken[1:] 107 | if n.catchAllChild == nil { 108 | n.catchAllChild = &node{path: thisToken, isCatchAll: true} 109 | } 110 | 111 | if path[1:] != n.catchAllChild.path { 112 | panic(fmt.Sprintf("Catch-all name in %s doesn't match %s. You probably tried to define overlapping catchalls", 113 | path, n.catchAllChild.path)) 114 | } 115 | 116 | if nextSlash != -1 { 117 | panic("/ after catch-all found in " + path) 118 | } 119 | 120 | if wildcards == nil { 121 | wildcards = []string{thisToken} 122 | } else { 123 | wildcards = append(wildcards, thisToken) 124 | } 125 | n.catchAllChild.leafWildcardNames = wildcards 126 | 127 | return n.catchAllChild 128 | } else if c == ':' && !inStaticToken { 129 | // Token starts with a : 130 | thisToken = thisToken[1:] 131 | 132 | if wildcards == nil { 133 | wildcards = []string{thisToken} 134 | } else { 135 | wildcards = append(wildcards, thisToken) 136 | } 137 | 138 | if n.wildcardChild == nil { 139 | n.wildcardChild = &node{path: "wildcard"} 140 | } 141 | 142 | return n.wildcardChild.addPath(remainingPath, wildcards, false) 143 | 144 | } else { 145 | // if strings.ContainsAny(thisToken, ":*") { 146 | // panic("* or : in middle of path component " + path) 147 | // } 148 | 149 | unescaped := false 150 | if len(thisToken) >= 2 && !inStaticToken { 151 | if thisToken[0] == '\\' && (thisToken[1] == '*' || thisToken[1] == ':' || thisToken[1] == '\\') { 152 | // The token starts with a character escaped by a backslash. Drop the backslash. 153 | c = thisToken[1] 154 | thisToken = thisToken[1:] 155 | unescaped = true 156 | } 157 | } 158 | 159 | // Set inStaticToken to ensure that the rest of this token is not mistaken 160 | // for a wildcard if a prefix split occurs at a '*' or ':'. 161 | inStaticToken = (c != '/') 162 | 163 | // Do we have an existing node that starts with the same letter? 164 | for i, index := range n.staticIndices { 165 | if c == index { 166 | // Yes. Split it based on the common prefix of the existing 167 | // node and the new one. 168 | child, prefixSplit := n.splitCommonPrefix(i, thisToken) 169 | 170 | child.priority++ 171 | n.sortStaticChild(i) 172 | if unescaped { 173 | // Account for the removed backslash. 174 | prefixSplit++ 175 | } 176 | return child.addPath(path[prefixSplit:], wildcards, inStaticToken) 177 | } 178 | } 179 | 180 | // No existing node starting with this letter, so create it. 181 | child := &node{path: thisToken} 182 | 183 | if n.staticIndices == nil { 184 | n.staticIndices = []byte{c} 185 | n.staticChild = []*node{child} 186 | } else { 187 | n.staticIndices = append(n.staticIndices, c) 188 | n.staticChild = append(n.staticChild, child) 189 | } 190 | return child.addPath(remainingPath, wildcards, inStaticToken) 191 | } 192 | } 193 | 194 | func (n *node) splitCommonPrefix(existingNodeIndex int, path string) (*node, int) { 195 | childNode := n.staticChild[existingNodeIndex] 196 | 197 | if strings.HasPrefix(path, childNode.path) { 198 | // No split needs to be done. Rather, the new path shares the entire 199 | // prefix with the existing node, so the new node is just a child of 200 | // the existing one. Or the new path is the same as the existing path, 201 | // which means that we just move on to the next token. Either way, 202 | // this return accomplishes that 203 | return childNode, len(childNode.path) 204 | } 205 | 206 | var i int 207 | // Find the length of the common prefix of the child node and the new path. 208 | for i = range childNode.path { 209 | if i == len(path) { 210 | break 211 | } 212 | if path[i] != childNode.path[i] { 213 | break 214 | } 215 | } 216 | 217 | commonPrefix := path[0:i] 218 | childNode.path = childNode.path[i:] 219 | 220 | // Create a new intermediary node in the place of the existing node, with 221 | // the existing node as a child. 222 | newNode := &node{ 223 | path: commonPrefix, 224 | priority: childNode.priority, 225 | // Index is the first letter of the non-common part of the path. 226 | staticIndices: []byte{childNode.path[0]}, 227 | staticChild: []*node{childNode}, 228 | } 229 | n.staticChild[existingNodeIndex] = newNode 230 | 231 | return newNode, i 232 | } 233 | 234 | func (n *node) search(method, path string) (found *node, handler HandlerFunc, params []string) { 235 | // if test != nil { 236 | // test.Logf("Searching for %s in %s", path, n.dumpTree("", "")) 237 | // } 238 | pathLen := len(path) 239 | if pathLen == 0 { 240 | if len(n.leafHandler) == 0 { 241 | return nil, nil, nil 242 | } else { 243 | return n, n.leafHandler[method], nil 244 | } 245 | } 246 | 247 | // First see if this matches a static token. 248 | firstChar := path[0] 249 | for i, staticIndex := range n.staticIndices { 250 | if staticIndex == firstChar { 251 | child := n.staticChild[i] 252 | childPathLen := len(child.path) 253 | if pathLen >= childPathLen && child.path == path[:childPathLen] { 254 | nextPath := path[childPathLen:] 255 | found, handler, params = child.search(method, nextPath) 256 | } 257 | break 258 | } 259 | } 260 | 261 | // If we found a node and it had a valid handler, then return here. Otherwise 262 | // let's remember that we found this one, but look for a better match. 263 | if handler != nil { 264 | return 265 | } 266 | 267 | if n.wildcardChild != nil { 268 | // Didn't find a static token, so check for a wildcard. 269 | nextSlash := strings.IndexByte(path, '/') 270 | if nextSlash < 0 { 271 | nextSlash = pathLen 272 | } 273 | 274 | thisToken := path[0:nextSlash] 275 | nextToken := path[nextSlash:] 276 | 277 | if len(thisToken) > 0 { // Don't match on empty tokens. 278 | wcNode, wcHandler, wcParams := n.wildcardChild.search(method, nextToken) 279 | if wcHandler != nil || (found == nil && wcNode != nil) { 280 | unescaped, err := unescape(thisToken) 281 | if err != nil { 282 | unescaped = thisToken 283 | } 284 | 285 | if wcParams == nil { 286 | wcParams = []string{unescaped} 287 | } else { 288 | wcParams = append(wcParams, unescaped) 289 | } 290 | 291 | if wcHandler != nil { 292 | return wcNode, wcHandler, wcParams 293 | } 294 | 295 | // Didn't actually find a handler here, so remember that we 296 | // found a node but also see if we can fall through to the 297 | // catchall. 298 | found = wcNode 299 | handler = wcHandler 300 | params = wcParams 301 | } 302 | } 303 | } 304 | 305 | catchAllChild := n.catchAllChild 306 | if catchAllChild != nil { 307 | // Hit the catchall, so just assign the whole remaining path if it 308 | // has a matching handler. 309 | handler = catchAllChild.leafHandler[method] 310 | // Found a handler, or we found a catchall node without a handler. 311 | // Either way, return it since there's nothing left to check after this. 312 | if handler != nil || found == nil { 313 | unescaped, err := unescape(path) 314 | if err != nil { 315 | unescaped = path 316 | } 317 | 318 | return catchAllChild, handler, []string{unescaped} 319 | } 320 | 321 | } 322 | 323 | return found, handler, params 324 | } 325 | 326 | func (n *node) dumpTree(prefix, nodeType string) string { 327 | line := fmt.Sprintf("%s %02d %s%s [%d] %v wildcards %v\n", prefix, n.priority, nodeType, n.path, 328 | len(n.staticChild), n.leafHandler, n.leafWildcardNames) 329 | prefix += " " 330 | for _, node := range n.staticChild { 331 | line += node.dumpTree(prefix, "") 332 | } 333 | if n.wildcardChild != nil { 334 | line += n.wildcardChild.dumpTree(prefix, ":") 335 | } 336 | if n.catchAllChild != nil { 337 | line += n.catchAllChild.dumpTree(prefix, "*") 338 | } 339 | return line 340 | } 341 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | // This is inspired by Julien Schmidt's httprouter, in that it uses a patricia tree, but the 2 | // implementation is rather different. Specifically, the routing rules are relaxed so that a 3 | // single path segment may be a wildcard in one route and a static token in another. This gives a 4 | // nice combination of high performance with a lot of convenience in designing the routing patterns. 5 | package httptreemux 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | // The params argument contains the parameters parsed from wildcards and catch-alls in the URL. 15 | type HandlerFunc func(http.ResponseWriter, *http.Request, map[string]string) 16 | type PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 17 | 18 | // RedirectBehavior sets the behavior when the router redirects the request to the 19 | // canonical version of the requested URL using RedirectTrailingSlash or RedirectClean. 20 | // The default behavior is to return a 301 status, redirecting the browser to the version 21 | // of the URL that matches the given pattern. 22 | // 23 | // On a POST request, most browsers that receive a 301 will submit a GET request to 24 | // the redirected URL, meaning that any data will likely be lost. If you want to handle 25 | // and avoid this behavior, you may use Redirect307, which causes most browsers to 26 | // resubmit the request using the original method and request body. 27 | // 28 | // Since 307 is supposed to be a temporary redirect, the new 308 status code has been 29 | // proposed, which is treated the same, except it indicates correctly that the redirection 30 | // is permanent. The big caveat here is that the RFC is relatively recent, and older 31 | // browsers will not know what to do with it. Therefore its use is not recommended 32 | // unless you really know what you're doing. 33 | // 34 | // Finally, the UseHandler value will simply call the handler function for the pattern. 35 | type RedirectBehavior int 36 | 37 | type PathSource int 38 | 39 | const ( 40 | Redirect301 RedirectBehavior = iota // Return 301 Moved Permanently 41 | Redirect307 // Return 307 HTTP/1.1 Temporary Redirect 42 | Redirect308 // Return a 308 RFC7538 Permanent Redirect 43 | UseHandler // Just call the handler function 44 | 45 | RequestURI PathSource = iota // Use r.RequestURI 46 | URLPath // Use r.URL.Path 47 | ) 48 | 49 | // LookupResult contains information about a route lookup, which is returned from Lookup and 50 | // can be passed to ServeLookupResult if the request should be served. 51 | type LookupResult struct { 52 | // StatusCode informs the caller about the result of the lookup. 53 | // This will generally be `http.StatusNotFound` or `http.StatusMethodNotAllowed` for an 54 | // error case. On a normal success, the statusCode will be `http.StatusOK`. A redirect code 55 | // will also be used in the case 56 | StatusCode int 57 | handler HandlerFunc 58 | // Params represents the key value pairs of the path parameters. 59 | Params map[string]string 60 | leafHandler map[string]HandlerFunc // Only has a value when StatusCode is MethodNotAllowed. 61 | } 62 | 63 | // Dump returns a text representation of the routing tree. 64 | func (t *TreeMux) Dump() string { 65 | return t.root.dumpTree("", "") 66 | } 67 | 68 | func (t *TreeMux) serveHTTPPanic(w http.ResponseWriter, r *http.Request) { 69 | if err := recover(); err != nil { 70 | t.PanicHandler(w, r, err) 71 | } 72 | } 73 | 74 | func (t *TreeMux) redirectStatusCode(method string) (int, bool) { 75 | var behavior RedirectBehavior 76 | var ok bool 77 | if behavior, ok = t.RedirectMethodBehavior[method]; !ok { 78 | behavior = t.RedirectBehavior 79 | } 80 | switch behavior { 81 | case Redirect301: 82 | return http.StatusMovedPermanently, true 83 | case Redirect307: 84 | return http.StatusTemporaryRedirect, true 85 | case Redirect308: 86 | // Go doesn't have a constant for this yet. Yet another sign 87 | // that you probably shouldn't use it. 88 | return 308, true 89 | case UseHandler: 90 | return 0, false 91 | default: 92 | return http.StatusMovedPermanently, true 93 | } 94 | } 95 | 96 | func redirectHandler(newPath string, statusCode int) HandlerFunc { 97 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 98 | redirect(w, r, newPath, statusCode) 99 | } 100 | } 101 | 102 | func redirect(w http.ResponseWriter, r *http.Request, newPath string, statusCode int) { 103 | newURL := url.URL{ 104 | Path: newPath, 105 | RawQuery: r.URL.RawQuery, 106 | Fragment: r.URL.Fragment, 107 | } 108 | http.Redirect(w, r, newURL.String(), statusCode) 109 | } 110 | 111 | func (t *TreeMux) lookup(w http.ResponseWriter, r *http.Request) (result LookupResult, found bool) { 112 | result.StatusCode = http.StatusNotFound 113 | path := r.RequestURI 114 | unescapedPath := r.URL.Path 115 | pathLen := len(path) 116 | if pathLen > 0 && t.PathSource == RequestURI { 117 | rawQueryLen := len(r.URL.RawQuery) 118 | 119 | if rawQueryLen != 0 || path[pathLen-1] == '?' { 120 | // Remove any query string and the ?. 121 | path = path[:pathLen-rawQueryLen-1] 122 | pathLen = len(path) 123 | } 124 | } else { 125 | // In testing with http.NewRequest, 126 | // RequestURI is not set so just grab URL.Path instead. 127 | path = r.URL.Path 128 | pathLen = len(path) 129 | } 130 | if t.CaseInsensitive { 131 | path = strings.ToLower(path) 132 | unescapedPath = strings.ToLower(unescapedPath) 133 | } 134 | 135 | trailingSlash := path[pathLen-1] == '/' && pathLen > 1 136 | if trailingSlash && t.RedirectTrailingSlash { 137 | path = path[:pathLen-1] 138 | unescapedPath = unescapedPath[:len(unescapedPath)-1] 139 | } 140 | 141 | n, handler, params := t.root.search(r.Method, path[1:]) 142 | if n == nil { 143 | if t.RedirectCleanPath { 144 | // Path was not found. Try cleaning it up and search again. 145 | // TODO Test this 146 | cleanPath := Clean(unescapedPath) 147 | n, handler, params = t.root.search(r.Method, cleanPath[1:]) 148 | if n == nil { 149 | // Still nothing found. 150 | return 151 | } 152 | if statusCode, ok := t.redirectStatusCode(r.Method); ok { 153 | // Redirect to the actual path 154 | return LookupResult{statusCode, redirectHandler(cleanPath, statusCode), nil, nil}, true 155 | } 156 | } else { 157 | // Not found. 158 | return 159 | } 160 | } 161 | 162 | if handler == nil { 163 | if r.Method == "OPTIONS" && t.OptionsHandler != nil { 164 | handler = t.OptionsHandler 165 | } 166 | 167 | if handler == nil { 168 | result.leafHandler = n.leafHandler 169 | result.StatusCode = http.StatusMethodNotAllowed 170 | return 171 | } 172 | } 173 | 174 | if !n.isCatchAll || t.RemoveCatchAllTrailingSlash { 175 | if trailingSlash != n.addSlash && t.RedirectTrailingSlash { 176 | if statusCode, ok := t.redirectStatusCode(r.Method); ok { 177 | var h HandlerFunc 178 | if n.addSlash { 179 | // Need to add a slash. 180 | h = redirectHandler(unescapedPath+"/", statusCode) 181 | } else if path != "/" { 182 | // We need to remove the slash. This was already done at the 183 | // beginning of the function. 184 | h = redirectHandler(unescapedPath, statusCode) 185 | } 186 | 187 | if h != nil { 188 | return LookupResult{statusCode, h, nil, nil}, true 189 | } 190 | } 191 | } 192 | } 193 | 194 | var paramMap map[string]string 195 | if len(params) != 0 { 196 | if len(params) != len(n.leafWildcardNames) { 197 | // Need better behavior here. Should this be a panic? 198 | panic(fmt.Sprintf("httptreemux parameter list length mismatch: %v, %v", 199 | params, n.leafWildcardNames)) 200 | } 201 | 202 | paramMap = make(map[string]string) 203 | numParams := len(params) 204 | for index := 0; index < numParams; index++ { 205 | paramMap[n.leafWildcardNames[numParams-index-1]] = params[index] 206 | } 207 | } 208 | 209 | return LookupResult{http.StatusOK, handler, paramMap, nil}, true 210 | } 211 | 212 | // Lookup performs a lookup without actually serving the request or mutating the request or response. 213 | // The return values are a LookupResult and a boolean. The boolean will be true when a handler 214 | // was found or the lookup resulted in a redirect which will point to a real handler. It is false 215 | // for requests which would result in a `StatusNotFound` or `StatusMethodNotAllowed`. 216 | // 217 | // Regardless of the returned boolean's value, the LookupResult may be passed to ServeLookupResult 218 | // to be served appropriately. 219 | func (t *TreeMux) Lookup(w http.ResponseWriter, r *http.Request) (LookupResult, bool) { 220 | if t.SafeAddRoutesWhileRunning { 221 | // In concurrency safe mode, we acquire a read lock on the mutex for any access. 222 | // This is optional to avoid potential performance loss in high-usage scenarios. 223 | t.mutex.RLock() 224 | } 225 | 226 | result, found := t.lookup(w, r) 227 | 228 | if t.SafeAddRoutesWhileRunning { 229 | t.mutex.RUnlock() 230 | } 231 | 232 | return result, found 233 | } 234 | 235 | // ServeLookupResult serves a request, given a lookup result from the Lookup function. 236 | func (t *TreeMux) ServeLookupResult(w http.ResponseWriter, r *http.Request, lr LookupResult) { 237 | if lr.handler == nil { 238 | if lr.StatusCode == http.StatusMethodNotAllowed && lr.leafHandler != nil { 239 | if t.SafeAddRoutesWhileRunning { 240 | t.mutex.RLock() 241 | } 242 | 243 | t.MethodNotAllowedHandler(w, r, lr.leafHandler) 244 | 245 | if t.SafeAddRoutesWhileRunning { 246 | t.mutex.RUnlock() 247 | } 248 | } else { 249 | t.NotFoundHandler(w, r) 250 | } 251 | } else { 252 | r = t.setDefaultRequestContext(r) 253 | lr.handler(w, r, lr.Params) 254 | } 255 | } 256 | 257 | func (t *TreeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 258 | if t.PanicHandler != nil { 259 | defer t.serveHTTPPanic(w, r) 260 | } 261 | 262 | if t.SafeAddRoutesWhileRunning { 263 | // In concurrency safe mode, we acquire a read lock on the mutex for any access. 264 | // This is optional to avoid potential performance loss in high-usage scenarios. 265 | t.mutex.RLock() 266 | } 267 | 268 | result, _ := t.lookup(w, r) 269 | 270 | if t.SafeAddRoutesWhileRunning { 271 | t.mutex.RUnlock() 272 | } 273 | 274 | t.ServeLookupResult(w, r, result) 275 | } 276 | 277 | // MethodNotAllowedHandler is the default handler for TreeMux.MethodNotAllowedHandler, 278 | // which is called for patterns that match, but do not have a handler installed for the 279 | // requested method. It simply writes the status code http.StatusMethodNotAllowed and fills 280 | // in the `Allow` header value appropriately. 281 | func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request, 282 | methods map[string]HandlerFunc) { 283 | 284 | for m := range methods { 285 | w.Header().Add("Allow", m) 286 | } 287 | 288 | w.WriteHeader(http.StatusMethodNotAllowed) 289 | } 290 | 291 | func New() *TreeMux { 292 | tm := &TreeMux{ 293 | root: &node{path: "/"}, 294 | NotFoundHandler: http.NotFound, 295 | MethodNotAllowedHandler: MethodNotAllowedHandler, 296 | HeadCanUseGet: true, 297 | RedirectTrailingSlash: true, 298 | RedirectCleanPath: true, 299 | RedirectBehavior: Redirect301, 300 | RedirectMethodBehavior: make(map[string]RedirectBehavior), 301 | PathSource: RequestURI, 302 | EscapeAddedRoutes: false, 303 | } 304 | tm.Group.mux = tm 305 | return tm 306 | } 307 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func dummyHandler(w http.ResponseWriter, r *http.Request, urlParams map[string]string) { 9 | 10 | } 11 | 12 | func addPath(t *testing.T, tree *node, path string) { 13 | t.Logf("Adding path %s", path) 14 | n := tree.addPath(path[1:], nil, false) 15 | handler := func(w http.ResponseWriter, r *http.Request, urlParams map[string]string) { 16 | urlParams["path"] = path 17 | } 18 | n.setHandler("GET", handler, false) 19 | } 20 | 21 | var test *testing.T 22 | 23 | func testPath(t *testing.T, tree *node, path string, expectPath string, expectedParams map[string]string) { 24 | if t.Failed() { 25 | t.Log(tree.dumpTree("", " ")) 26 | t.FailNow() 27 | } 28 | 29 | t.Log("Testing", path) 30 | n, foundHandler, paramList := tree.search("GET", path[1:]) 31 | if expectPath != "" && n == nil { 32 | t.Errorf("No match for %s, expected %s", path, expectPath) 33 | return 34 | } else if expectPath == "" && n != nil { 35 | t.Errorf("Expected no match for %s but got %v with params %v", path, n, expectedParams) 36 | t.Error("Node and subtree was\n" + n.dumpTree("", " ")) 37 | return 38 | } 39 | 40 | if n == nil { 41 | return 42 | } 43 | 44 | handler, ok := n.leafHandler["GET"] 45 | if !ok { 46 | t.Errorf("Path %s returned node without handler", path) 47 | t.Error("Node and subtree was\n" + n.dumpTree("", " ")) 48 | return 49 | } 50 | 51 | if foundHandler == nil { 52 | t.Errorf("Path %s returned valid node but foundHandler was false", path) 53 | t.Error("Node and subtree was\n" + n.dumpTree("", " ")) 54 | return 55 | } 56 | 57 | pathMap := make(map[string]string) 58 | handler(nil, nil, pathMap) 59 | matchedPath := pathMap["path"] 60 | 61 | if matchedPath != expectPath { 62 | t.Errorf("Path %s matched %s, expected %s", path, matchedPath, expectPath) 63 | t.Error("Node and subtree was\n" + n.dumpTree("", " ")) 64 | } 65 | 66 | if expectedParams == nil { 67 | if len(paramList) != 0 { 68 | t.Errorf("Path %s expected no parameters, saw %v", path, paramList) 69 | } 70 | } else { 71 | if len(paramList) != len(n.leafWildcardNames) { 72 | t.Errorf("Got %d params back but node specifies %d", 73 | len(paramList), len(n.leafWildcardNames)) 74 | } 75 | 76 | params := map[string]string{} 77 | for i := 0; i < len(paramList); i++ { 78 | params[n.leafWildcardNames[len(paramList)-i-1]] = paramList[i] 79 | } 80 | t.Log("\tGot params", params) 81 | 82 | for key, val := range expectedParams { 83 | sawVal, ok := params[key] 84 | if !ok { 85 | t.Errorf("Path %s matched without key %s", path, key) 86 | } else if sawVal != val { 87 | t.Errorf("Path %s expected param %s to be %s, saw %s", path, key, val, sawVal) 88 | } 89 | 90 | delete(params, key) 91 | } 92 | 93 | for key, val := range params { 94 | t.Errorf("Path %s returned unexpected param %s=%s", path, key, val) 95 | } 96 | } 97 | 98 | } 99 | 100 | func checkHandlerNodes(t *testing.T, n *node) { 101 | hasHandlers := len(n.leafHandler) != 0 102 | hasWildcards := len(n.leafWildcardNames) != 0 103 | 104 | if hasWildcards && !hasHandlers { 105 | t.Errorf("Node %s has wildcards without handlers", n.path) 106 | } 107 | } 108 | 109 | func TestTree(t *testing.T) { 110 | test = t 111 | tree := &node{path: "/"} 112 | 113 | addPath(t, tree, "/") 114 | addPath(t, tree, "/i") 115 | addPath(t, tree, "/i/:aaa") 116 | addPath(t, tree, "/images") 117 | addPath(t, tree, "/images/abc.jpg") 118 | addPath(t, tree, "/images/:imgname") 119 | addPath(t, tree, "/images/\\*path") 120 | addPath(t, tree, "/images/\\*patch") 121 | addPath(t, tree, "/images/*path") 122 | addPath(t, tree, "/ima") 123 | addPath(t, tree, "/ima/:par") 124 | addPath(t, tree, "/images1") 125 | addPath(t, tree, "/images2") 126 | addPath(t, tree, "/apples") 127 | addPath(t, tree, "/app/les") 128 | addPath(t, tree, "/apples1") 129 | addPath(t, tree, "/appeasement") 130 | addPath(t, tree, "/appealing") 131 | addPath(t, tree, "/date/\\:year/\\:month") 132 | addPath(t, tree, "/date/:year/:month") 133 | addPath(t, tree, "/date/:year/month") 134 | addPath(t, tree, "/date/:year/:month/abc") 135 | addPath(t, tree, "/date/:year/:month/:post") 136 | addPath(t, tree, "/date/:year/:month/*post") 137 | addPath(t, tree, "/:page") 138 | addPath(t, tree, "/:page/:index") 139 | addPath(t, tree, "/post/:post/page/:page") 140 | addPath(t, tree, "/plaster") 141 | addPath(t, tree, "/users/:pk/:related") 142 | addPath(t, tree, "/users/:id/updatePassword") 143 | addPath(t, tree, "/:something/abc") 144 | addPath(t, tree, "/:something/def") 145 | addPath(t, tree, "/apples/ab:cde/:fg/*hi") 146 | addPath(t, tree, "/apples/ab*cde/:fg/*hi") 147 | addPath(t, tree, "/apples/ab\\*cde/:fg/*hi") 148 | addPath(t, tree, "/apples/ab*dde") 149 | 150 | testPath(t, tree, "/users/abc/updatePassword", "/users/:id/updatePassword", 151 | map[string]string{"id": "abc"}) 152 | testPath(t, tree, "/users/all/something", "/users/:pk/:related", 153 | map[string]string{"pk": "all", "related": "something"}) 154 | 155 | testPath(t, tree, "/aaa/abc", "/:something/abc", 156 | map[string]string{"something": "aaa"}) 157 | testPath(t, tree, "/aaa/def", "/:something/def", 158 | map[string]string{"something": "aaa"}) 159 | 160 | testPath(t, tree, "/paper", "/:page", 161 | map[string]string{"page": "paper"}) 162 | 163 | testPath(t, tree, "/", "/", nil) 164 | testPath(t, tree, "/i", "/i", nil) 165 | testPath(t, tree, "/images", "/images", nil) 166 | testPath(t, tree, "/images/abc.jpg", "/images/abc.jpg", nil) 167 | testPath(t, tree, "/images/something", "/images/:imgname", 168 | map[string]string{"imgname": "something"}) 169 | testPath(t, tree, "/images/long/path", "/images/*path", 170 | map[string]string{"path": "long/path"}) 171 | testPath(t, tree, "/images/even/longer/path", "/images/*path", 172 | map[string]string{"path": "even/longer/path"}) 173 | testPath(t, tree, "/ima", "/ima", nil) 174 | testPath(t, tree, "/apples", "/apples", nil) 175 | testPath(t, tree, "/app/les", "/app/les", nil) 176 | testPath(t, tree, "/abc", "/:page", 177 | map[string]string{"page": "abc"}) 178 | testPath(t, tree, "/abc/100", "/:page/:index", 179 | map[string]string{"page": "abc", "index": "100"}) 180 | testPath(t, tree, "/post/a/page/2", "/post/:post/page/:page", 181 | map[string]string{"post": "a", "page": "2"}) 182 | testPath(t, tree, "/date/2014/5", "/date/:year/:month", 183 | map[string]string{"year": "2014", "month": "5"}) 184 | testPath(t, tree, "/date/2014/month", "/date/:year/month", 185 | map[string]string{"year": "2014"}) 186 | testPath(t, tree, "/date/2014/5/abc", "/date/:year/:month/abc", 187 | map[string]string{"year": "2014", "month": "5"}) 188 | testPath(t, tree, "/date/2014/5/def", "/date/:year/:month/:post", 189 | map[string]string{"year": "2014", "month": "5", "post": "def"}) 190 | testPath(t, tree, "/date/2014/5/def/hij", "/date/:year/:month/*post", 191 | map[string]string{"year": "2014", "month": "5", "post": "def/hij"}) 192 | testPath(t, tree, "/date/2014/5/def/hij/", "/date/:year/:month/*post", 193 | map[string]string{"year": "2014", "month": "5", "post": "def/hij/"}) 194 | 195 | testPath(t, tree, "/date/2014/ab%2f", "/date/:year/:month", 196 | map[string]string{"year": "2014", "month": "ab/"}) 197 | testPath(t, tree, "/post/ab%2fdef/page/2%2f", "/post/:post/page/:page", 198 | map[string]string{"post": "ab/def", "page": "2/"}) 199 | 200 | // Test paths with escaped wildcard characters. 201 | testPath(t, tree, "/images/*path", "/images/\\*path", nil) 202 | testPath(t, tree, "/images/*patch", "/images/\\*patch", nil) 203 | testPath(t, tree, "/date/:year/:month", "/date/\\:year/\\:month", nil) 204 | testPath(t, tree, "/apples/ab*cde/lala/baba/dada", "/apples/ab*cde/:fg/*hi", 205 | map[string]string{"fg": "lala", "hi": "baba/dada"}) 206 | testPath(t, tree, "/apples/ab\\*cde/lala/baba/dada", "/apples/ab\\*cde/:fg/*hi", 207 | map[string]string{"fg": "lala", "hi": "baba/dada"}) 208 | testPath(t, tree, "/apples/ab:cde/:fg/*hi", "/apples/ab:cde/:fg/*hi", 209 | map[string]string{"fg": ":fg", "hi": "*hi"}) 210 | testPath(t, tree, "/apples/ab*cde/:fg/*hi", "/apples/ab*cde/:fg/*hi", 211 | map[string]string{"fg": ":fg", "hi": "*hi"}) 212 | testPath(t, tree, "/apples/ab*cde/one/two/three", "/apples/ab*cde/:fg/*hi", 213 | map[string]string{"fg": "one", "hi": "two/three"}) 214 | testPath(t, tree, "/apples/ab*dde", "/apples/ab*dde", nil) 215 | 216 | testPath(t, tree, "/ima/bcd/fgh", "", nil) 217 | testPath(t, tree, "/date/2014//month", "", nil) 218 | testPath(t, tree, "/date/2014/05/", "", nil) // Empty catchall should not match 219 | testPath(t, tree, "/post//abc/page/2", "", nil) 220 | testPath(t, tree, "/post/abc//page/2", "", nil) 221 | testPath(t, tree, "/post/abc/page//2", "", nil) 222 | testPath(t, tree, "//post/abc/page/2", "", nil) 223 | testPath(t, tree, "//post//abc//page//2", "", nil) 224 | 225 | t.Log("Test retrieval of duplicate paths") 226 | params := make(map[string]string) 227 | p := "date/:year/:month/abc" 228 | n := tree.addPath(p, nil, false) 229 | if n == nil { 230 | t.Errorf("Duplicate add of %s didn't return a node", p) 231 | } else { 232 | handler, ok := n.leafHandler["GET"] 233 | matchPath := "" 234 | if ok { 235 | handler(nil, nil, params) 236 | matchPath = params["path"] 237 | } 238 | 239 | if len(matchPath) < 2 || matchPath[1:] != p { 240 | t.Errorf("Duplicate add of %s returned node for %s\n%s", p, matchPath, 241 | n.dumpTree("", " ")) 242 | 243 | } 244 | } 245 | 246 | checkHandlerNodes(t, tree) 247 | 248 | t.Log(tree.dumpTree("", " ")) 249 | test = nil 250 | } 251 | 252 | func TestPanics(t *testing.T) { 253 | sawPanic := false 254 | 255 | panicHandler := func() { 256 | if err := recover(); err != nil { 257 | sawPanic = true 258 | } 259 | } 260 | 261 | addPathPanic := func(p ...string) { 262 | sawPanic = false 263 | defer panicHandler() 264 | tree := &node{path: "/"} 265 | for _, path := range p { 266 | tree.addPath(path, nil, false) 267 | } 268 | } 269 | 270 | addPathPanic("abc/*path/") 271 | if !sawPanic { 272 | t.Error("Expected panic with slash after catch-all") 273 | } 274 | 275 | addPathPanic("abc/*path/def") 276 | if !sawPanic { 277 | t.Error("Expected panic with path segment after catch-all") 278 | } 279 | 280 | addPathPanic("abc/*path", "abc/*paths") 281 | if !sawPanic { 282 | t.Error("Expected panic when adding conflicting catch-alls") 283 | } 284 | 285 | func() { 286 | sawPanic = false 287 | defer panicHandler() 288 | tree := &node{path: "/"} 289 | tree.setHandler("GET", dummyHandler, false) 290 | tree.setHandler("GET", dummyHandler, false) 291 | }() 292 | if !sawPanic { 293 | t.Error("Expected panic when adding a duplicate handler for a pattern") 294 | } 295 | 296 | twoPathPanic := func(first, second string) { 297 | addPathPanic(first, second) 298 | if !sawPanic { 299 | t.Errorf("Expected panic with ambiguous wildcards on paths %s and %s", first, second) 300 | } 301 | } 302 | 303 | twoPathPanic("abc/:ab/def/:cd", "abc/:ad/def/:cd") 304 | twoPathPanic("abc/:ab/def/:cd", "abc/:ab/def/:ef") 305 | twoPathPanic(":abc", ":def") 306 | twoPathPanic(":abc/ggg", ":def/ggg") 307 | } 308 | 309 | func BenchmarkTreeNullRequest(b *testing.B) { 310 | b.ReportAllocs() 311 | tree := &node{ 312 | path: "/", 313 | leafHandler: map[string]HandlerFunc{ 314 | "GET": dummyHandler, 315 | }, 316 | } 317 | 318 | b.ResetTimer() 319 | for i := 0; i < b.N; i++ { 320 | tree.search("GET", "") 321 | } 322 | } 323 | 324 | func BenchmarkTreeOneStatic(b *testing.B) { 325 | b.ReportAllocs() 326 | tree := &node{ 327 | path: "/", 328 | leafHandler: map[string]HandlerFunc{ 329 | "GET": dummyHandler, 330 | }, 331 | } 332 | tree.addPath("abc", nil, false) 333 | 334 | b.ResetTimer() 335 | for i := 0; i < b.N; i++ { 336 | tree.search("GET", "abc") 337 | } 338 | } 339 | 340 | func BenchmarkTreeOneParam(b *testing.B) { 341 | tree := &node{ 342 | path: "/", 343 | leafHandler: map[string]HandlerFunc{ 344 | "GET": dummyHandler, 345 | }, 346 | } 347 | b.ReportAllocs() 348 | tree.addPath(":abc", nil, false) 349 | 350 | b.ResetTimer() 351 | for i := 0; i < b.N; i++ { 352 | tree.search("GET", "abc") 353 | } 354 | } 355 | 356 | func BenchmarkTreeLongParams(b *testing.B) { 357 | tree := &node{ 358 | path: "/", 359 | leafHandler: map[string]HandlerFunc{ 360 | "GET": dummyHandler, 361 | }, 362 | } 363 | b.ReportAllocs() 364 | tree.addPath(":abc/:def/:ghi", nil, false) 365 | 366 | b.ResetTimer() 367 | for i := 0; i < b.N; i++ { 368 | tree.search("GET", "abcdefghijklmnop/aaaabbbbccccddddeeeeffffgggg/hijkl") 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.7 2 | // +build go1.7 3 | 4 | package httptreemux 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | type IContextGroup interface { 16 | GET(path string, handler http.HandlerFunc) 17 | POST(path string, handler http.HandlerFunc) 18 | PUT(path string, handler http.HandlerFunc) 19 | PATCH(path string, handler http.HandlerFunc) 20 | DELETE(path string, handler http.HandlerFunc) 21 | HEAD(path string, handler http.HandlerFunc) 22 | OPTIONS(path string, handler http.HandlerFunc) 23 | 24 | NewContextGroup(path string) *ContextGroup 25 | NewGroup(path string) *ContextGroup 26 | } 27 | 28 | func TestContextParams(t *testing.T) { 29 | m := &contextData{ 30 | params: map[string]string{"id": "123"}, 31 | route: "", 32 | } 33 | 34 | ctx := context.WithValue(context.Background(), contextDataKey, m) 35 | 36 | params := ContextParams(ctx) 37 | if params == nil { 38 | t.Errorf("expected '%#v', but got '%#v'", m, params) 39 | } 40 | 41 | if v := params["id"]; v != "123" { 42 | t.Errorf("expected '%s', but got '%#v'", m.params["id"], params["id"]) 43 | } 44 | } 45 | 46 | func TestContextRoute(t *testing.T) { 47 | tests := []struct { 48 | name, 49 | expectedRoute string 50 | }{ 51 | { 52 | name: "basic", 53 | expectedRoute: "/base/path", 54 | }, 55 | { 56 | name: "params", 57 | expectedRoute: "/base/path/:id/items/:itemid", 58 | }, 59 | { 60 | name: "catch-all", 61 | expectedRoute: "/base/*path", 62 | }, 63 | { 64 | name: "empty", 65 | expectedRoute: "", 66 | }, 67 | } 68 | for _, test := range tests { 69 | t.Run(test.name, func(t *testing.T) { 70 | cd := &contextData{} 71 | if len(test.expectedRoute) > 0 { 72 | cd.route = test.expectedRoute 73 | } 74 | ctx := context.WithValue(context.Background(), contextDataKey, cd) 75 | 76 | gotRoute := ContextRoute(ctx) 77 | 78 | if test.expectedRoute != gotRoute { 79 | t.Errorf("ContextRoute didn't return the desired route\nexpected %s\ngot: %s", test.expectedRoute, gotRoute) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestContextData(t *testing.T) { 86 | p := &contextData{ 87 | route: "route/path", 88 | params: map[string]string{"id": "123"}, 89 | } 90 | 91 | ctx := context.WithValue(context.Background(), contextDataKey, p) 92 | 93 | ctxData := ContextData(ctx) 94 | pathValue := ctxData.Route() 95 | if pathValue != p.route { 96 | t.Errorf("expected '%s', but got '%s'", p, pathValue) 97 | } 98 | 99 | params := ctxData.Params() 100 | if v := params["id"]; v != "123" { 101 | t.Errorf("expected '%s', but got '%#v'", p.params["id"], params["id"]) 102 | } 103 | } 104 | 105 | func TestContextDataWithEmptyParams(t *testing.T) { 106 | p := &contextData{ 107 | route: "route/path", 108 | params: nil, 109 | } 110 | 111 | ctx := context.WithValue(context.Background(), contextDataKey, p) 112 | params := ContextData(ctx).Params() 113 | if params == nil { 114 | t.Errorf("ContextData.Params should never return nil") 115 | } 116 | } 117 | 118 | func TestContextGroupMethods(t *testing.T) { 119 | for _, scenario := range scenarios { 120 | t.Run(scenario.description, func(t *testing.T) { 121 | testContextGroupMethods(t, scenario.RequestCreator, true, false) 122 | testContextGroupMethods(t, scenario.RequestCreator, false, false) 123 | testContextGroupMethods(t, scenario.RequestCreator, true, true) 124 | testContextGroupMethods(t, scenario.RequestCreator, false, true) 125 | }) 126 | } 127 | } 128 | 129 | func testContextGroupMethods(t *testing.T, reqGen RequestCreator, headCanUseGet bool, useContextRouter bool) { 130 | t.Run(fmt.Sprintf("headCanUseGet %v, useContextRouter %v", headCanUseGet, useContextRouter), func(t *testing.T) { 131 | var result string 132 | makeHandler := func(method, expectedRoutePath string, hasParam bool) http.HandlerFunc { 133 | return func(w http.ResponseWriter, r *http.Request) { 134 | result = method 135 | 136 | // Test Legacy Accessor 137 | var v string 138 | v, ok := ContextParams(r.Context())["param"] 139 | if hasParam && !ok { 140 | t.Error("missing key 'param' in context from ContextParams") 141 | } 142 | 143 | ctxData := ContextData(r.Context()) 144 | if ctxData == nil { 145 | t.Fatal("context did not contain ContextData") 146 | } 147 | 148 | v, ok = ctxData.Params()["param"] 149 | if hasParam && !ok { 150 | t.Error("missing key 'param' in context from ContextData") 151 | } 152 | 153 | routePath := ctxData.Route() 154 | if routePath != expectedRoutePath { 155 | t.Errorf("Expected context to have route path '%s', saw %s", expectedRoutePath, routePath) 156 | } 157 | 158 | if headCanUseGet && (method == "GET" || v == "HEAD") { 159 | return 160 | } 161 | if hasParam && v != method { 162 | t.Errorf("invalid key 'param' in context; expected '%s' but got '%s'", method, v) 163 | } 164 | } 165 | } 166 | 167 | var router http.Handler 168 | var rootGroup IContextGroup 169 | 170 | if useContextRouter { 171 | root := NewContextMux() 172 | root.HeadCanUseGet = headCanUseGet 173 | t.Log(root.TreeMux.HeadCanUseGet) 174 | router = root 175 | rootGroup = root 176 | } else { 177 | root := New() 178 | root.HeadCanUseGet = headCanUseGet 179 | router = root 180 | rootGroup = root.UsingContext() 181 | } 182 | 183 | cg := rootGroup.NewGroup("/base").NewGroup("/user") 184 | cg.GET("/:param", makeHandler("GET", cg.group.path+"/:param", true)) 185 | cg.POST("/:param", makeHandler("POST", cg.group.path+"/:param", true)) 186 | cg.PATCH("/PATCH", makeHandler("PATCH", cg.group.path+"/PATCH", false)) 187 | cg.PUT("/:param", makeHandler("PUT", cg.group.path+"/:param", true)) 188 | cg.Handler("DELETE", "/:param", http.HandlerFunc(makeHandler("DELETE", cg.group.path+"/:param", true))) 189 | 190 | testMethod := func(method, expect string) { 191 | result = "" 192 | w := httptest.NewRecorder() 193 | r, _ := reqGen(method, "/base/user/"+method, nil) 194 | router.ServeHTTP(w, r) 195 | if expect == "" && w.Code != http.StatusMethodNotAllowed { 196 | t.Errorf("Method %s not expected to match but saw code %d", method, w.Code) 197 | } 198 | 199 | if result != expect { 200 | t.Errorf("Method %s got result %s", method, result) 201 | } 202 | } 203 | 204 | testMethod("GET", "GET") 205 | testMethod("POST", "POST") 206 | testMethod("PATCH", "PATCH") 207 | testMethod("PUT", "PUT") 208 | testMethod("DELETE", "DELETE") 209 | 210 | if headCanUseGet { 211 | t.Log("Test implicit HEAD with HeadCanUseGet = true") 212 | testMethod("HEAD", "GET") 213 | } else { 214 | t.Log("Test implicit HEAD with HeadCanUseGet = false") 215 | testMethod("HEAD", "") 216 | } 217 | 218 | cg.HEAD("/:param", makeHandler("HEAD", cg.group.path+"/:param", true)) 219 | testMethod("HEAD", "HEAD") 220 | }) 221 | } 222 | 223 | func TestNewContextGroup(t *testing.T) { 224 | router := New() 225 | group := router.NewGroup("/api") 226 | 227 | group.GET("/v1", func(w http.ResponseWriter, r *http.Request, params map[string]string) { 228 | w.Write([]byte(`200 OK GET /api/v1`)) 229 | }) 230 | 231 | group.UsingContext().GET("/v2", func(w http.ResponseWriter, r *http.Request) { 232 | w.Write([]byte(`200 OK GET /api/v2`)) 233 | }) 234 | 235 | tests := []struct { 236 | uri, expected string 237 | }{ 238 | {"/api/v1", "200 OK GET /api/v1"}, 239 | {"/api/v2", "200 OK GET /api/v2"}, 240 | } 241 | 242 | for _, tc := range tests { 243 | r, err := http.NewRequest("GET", tc.uri, nil) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | 248 | w := httptest.NewRecorder() 249 | router.ServeHTTP(w, r) 250 | 251 | if w.Code != http.StatusOK { 252 | t.Errorf("GET %s: expected %d, but got %d", tc.uri, http.StatusOK, w.Code) 253 | } 254 | if got := w.Body.String(); got != tc.expected { 255 | t.Errorf("GET %s : expected %q, but got %q", tc.uri, tc.expected, got) 256 | } 257 | 258 | } 259 | } 260 | 261 | type ContextGroupHandler struct{} 262 | 263 | // adhere to the http.Handler interface 264 | func (f ContextGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 265 | switch r.Method { 266 | case "GET": 267 | w.Write([]byte(`200 OK GET /api/v1`)) 268 | default: 269 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 270 | return 271 | } 272 | } 273 | 274 | func TestNewContextGroupHandler(t *testing.T) { 275 | router := New() 276 | group := router.NewGroup("/api") 277 | 278 | group.UsingContext().Handler("GET", "/v1", ContextGroupHandler{}) 279 | 280 | tests := []struct { 281 | uri, expected string 282 | }{ 283 | {"/api/v1", "200 OK GET /api/v1"}, 284 | } 285 | 286 | for _, tc := range tests { 287 | r, err := http.NewRequest("GET", tc.uri, nil) 288 | if err != nil { 289 | t.Fatal(err) 290 | } 291 | 292 | w := httptest.NewRecorder() 293 | router.ServeHTTP(w, r) 294 | 295 | if w.Code != http.StatusOK { 296 | t.Errorf("GET %s: expected %d, but got %d", tc.uri, http.StatusOK, w.Code) 297 | } 298 | if got := w.Body.String(); got != tc.expected { 299 | t.Errorf("GET %s : expected %q, but got %q", tc.uri, tc.expected, got) 300 | } 301 | } 302 | } 303 | 304 | func TestDefaultContext(t *testing.T) { 305 | router := New() 306 | ctx := context.WithValue(context.Background(), "abc", "def") 307 | expectContext := false 308 | 309 | router.GET("/abc", func(w http.ResponseWriter, r *http.Request, params map[string]string) { 310 | contextValue := r.Context().Value("abc") 311 | if expectContext { 312 | x, ok := contextValue.(string) 313 | if !ok || x != "def" { 314 | t.Errorf("Unexpected context key value: %+v", contextValue) 315 | } 316 | } else { 317 | if contextValue != nil { 318 | t.Errorf("Expected blank context but key had value %+v", contextValue) 319 | } 320 | } 321 | }) 322 | 323 | r, err := http.NewRequest("GET", "/abc", nil) 324 | if err != nil { 325 | t.Fatal(err) 326 | } 327 | w := httptest.NewRecorder() 328 | t.Log("Testing without DefaultContext") 329 | router.ServeHTTP(w, r) 330 | 331 | router.DefaultContext = ctx 332 | expectContext = true 333 | w = httptest.NewRecorder() 334 | t.Log("Testing with DefaultContext") 335 | router.ServeHTTP(w, r) 336 | } 337 | 338 | func TestContextMuxSimple(t *testing.T) { 339 | router := NewContextMux() 340 | ctx := context.WithValue(context.Background(), "abc", "def") 341 | expectContext := false 342 | 343 | router.GET("/abc", func(w http.ResponseWriter, r *http.Request) { 344 | contextValue := r.Context().Value("abc") 345 | if expectContext { 346 | x, ok := contextValue.(string) 347 | if !ok || x != "def" { 348 | t.Errorf("Unexpected context key value: %+v", contextValue) 349 | } 350 | } else { 351 | if contextValue != nil { 352 | t.Errorf("Expected blank context but key had value %+v", contextValue) 353 | } 354 | } 355 | }) 356 | 357 | r, err := http.NewRequest("GET", "/abc", nil) 358 | if err != nil { 359 | t.Fatal(err) 360 | } 361 | w := httptest.NewRecorder() 362 | t.Log("Testing without DefaultContext") 363 | router.ServeHTTP(w, r) 364 | 365 | router.DefaultContext = ctx 366 | expectContext = true 367 | w = httptest.NewRecorder() 368 | t.Log("Testing with DefaultContext") 369 | router.ServeHTTP(w, r) 370 | } 371 | 372 | func TestAddDataToContext(t *testing.T) { 373 | expectedRoute := "/expected/route" 374 | expectedParams := map[string]string{ 375 | "test": "expected", 376 | } 377 | 378 | ctx := AddRouteDataToContext(context.Background(), &contextData{ 379 | route: expectedRoute, 380 | params: expectedParams, 381 | }) 382 | 383 | if gotData, ok := ctx.Value(contextDataKey).(*contextData); ok && gotData != nil { 384 | if gotData.route != expectedRoute { 385 | t.Errorf("Did not retrieve the desired route. Expected: %s; Got: %s", expectedRoute, gotData.route) 386 | } 387 | if !reflect.DeepEqual(expectedParams, gotData.params) { 388 | t.Errorf("Did not retrieve the desired parameters. Expected: %#v; Got: %#v", expectedParams, gotData.params) 389 | } 390 | } else { 391 | t.Error("failed to retrieve context data") 392 | } 393 | } 394 | 395 | func TestAddParamsToContext(t *testing.T) { 396 | expectedParams := map[string]string{ 397 | "test": "expected", 398 | } 399 | 400 | ctx := AddParamsToContext(context.Background(), expectedParams) 401 | 402 | if gotData, ok := ctx.Value(contextDataKey).(*contextData); ok && gotData != nil { 403 | if !reflect.DeepEqual(expectedParams, gotData.params) { 404 | t.Errorf("Did not retrieve the desired parameters. Expected: %#v; Got: %#v", expectedParams, gotData.params) 405 | } 406 | } else { 407 | t.Error("failed to retrieve context data") 408 | } 409 | } 410 | 411 | func TestAddRouteToContext(t *testing.T) { 412 | expectedRoute := "/expected/route" 413 | 414 | ctx := AddRouteToContext(context.Background(), expectedRoute) 415 | 416 | if gotData, ok := ctx.Value(contextDataKey).(*contextData); ok && gotData != nil { 417 | if gotData.route != expectedRoute { 418 | t.Errorf("Did not retrieve the desired route. Expected: %s; Got: %s", expectedRoute, gotData.route) 419 | } 420 | } else { 421 | t.Error("failed to retrieve context data") 422 | } 423 | } 424 | 425 | func TestContextDataWithMiddleware(t *testing.T) { 426 | wantRoute := "/foo/:id/bar" 427 | wantParams := map[string]string{ 428 | "id": "15", 429 | } 430 | 431 | validateRequestAndParams := func(request *http.Request, params map[string]string, location string) { 432 | data := ContextData(request.Context()) 433 | if data == nil { 434 | t.Fatalf("ContextData returned nil in %s", location) 435 | } 436 | if data.Route() != wantRoute { 437 | t.Errorf("Unexpected route in %s. Got %s", location, data.Route()) 438 | } 439 | if !reflect.DeepEqual(data.Params(), wantParams) { 440 | t.Errorf("Unexpected context params in %s. Got %+v", location, data.Params()) 441 | } 442 | if !reflect.DeepEqual(params, wantParams) { 443 | t.Errorf("Unexpected handler params in %s. Got %+v", location, params) 444 | } 445 | } 446 | 447 | router := NewContextMux() 448 | router.Use(func(next HandlerFunc) HandlerFunc { 449 | return func(writer http.ResponseWriter, request *http.Request, m map[string]string) { 450 | t.Log("Testing Middleware") 451 | validateRequestAndParams(request, m, "middleware") 452 | next(writer, request, m) 453 | } 454 | }) 455 | 456 | router.GET(wantRoute, func(writer http.ResponseWriter, request *http.Request) { 457 | t.Log("Testing handler") 458 | validateRequestAndParams(request, ContextParams(request.Context()), "handler") 459 | writer.WriteHeader(http.StatusOK) 460 | }) 461 | 462 | w := httptest.NewRecorder() 463 | r, _ := http.NewRequest(http.MethodGet, "/foo/15/bar", nil) 464 | router.ServeHTTP(w, r) 465 | 466 | if w.Code != http.StatusOK { 467 | t.Fatalf("unexpected status code. got %d", w.Code) 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | httptreemux [![Build Status](https://travis-ci.org/dimfeld/httptreemux.png?branch=master)](https://travis-ci.org/dimfeld/httptreemux) [![GoDoc](https://godoc.org/github.com/dimfeld/httptreemux?status.svg)](https://godoc.org/github.com/dimfeld/httptreemux) 2 | =========== 3 | 4 | > [!WARNING] 5 | > This project is no longer maintained. 6 | 7 | High-speed, flexible, tree-based HTTP router for Go. 8 | 9 | This is inspired by [Julien Schmidt's httprouter](https://www.github.com/julienschmidt/httprouter), in that it uses a patricia tree, but the implementation is rather different. Specifically, the routing rules are relaxed so that a single path segment may be a wildcard in one route and a static token in another. This gives a nice combination of high performance with a lot of convenience in designing the routing patterns. In [benchmarks](https://github.com/julienschmidt/go-http-routing-benchmark), httptreemux is close to, but slightly slower than, httprouter. 10 | 11 | Release notes may be found using the [Github releases tab](https://github.com/dimfeld/httptreemux/releases). Version numbers are compatible with the [Semantic Versioning 2.0.0](http://semver.org/) convention, and a new release is made after every change to the code. 12 | 13 | ## Installing with Go Modules 14 | 15 | When using Go Modules, import this repository with `import "github.com/dimfeld/httptreemux/v5"` to ensure that you get the right version. 16 | 17 | ## Why? 18 | There are a lot of good routers out there. But looking at the ones that were really lightweight, I couldn't quite get something that fit with the route patterns I wanted. The code itself is simple enough, so I spent an evening writing this. 19 | 20 | ## Handler 21 | The handler is a simple function with the prototype `func(w http.ResponseWriter, r *http.Request, params map[string]string)`. The params argument contains the parameters parsed from wildcards and catch-alls in the URL, as described below. This type is aliased as httptreemux.HandlerFunc. 22 | 23 | ### Using http.HandlerFunc 24 | Due to the inclusion of the [context](https://godoc.org/context) package as of Go 1.7, `httptreemux` now supports handlers of type [http.HandlerFunc](https://godoc.org/net/http#HandlerFunc). There are two ways to enable this support. 25 | 26 | #### Adapting an Existing Router 27 | 28 | The `UsingContext` method will wrap the router or group in a new group at the same path, but adapted for use with `context` and `http.HandlerFunc`. 29 | 30 | ```go 31 | router := httptreemux.New() 32 | 33 | group := router.NewGroup("/api") 34 | group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) { 35 | id := params["id"] 36 | fmt.Fprintf(w, "GET /api/v1/%s", id) 37 | }) 38 | 39 | // UsingContext returns a version of the router or group with context support. 40 | ctxGroup := group.UsingContext() // sibling to 'group' node in tree 41 | ctxGroup.GET("/v2/:id", func(w http.ResponseWriter, r *http.Request) { 42 | ctxData := httptreemux.ContextData(r.Context()) 43 | params := ctxData.Params() 44 | id := params["id"] 45 | 46 | // Useful for middleware to see which route was hit without dealing with wildcards 47 | routePath := ctxData.Route() 48 | 49 | // Prints GET /api/v2/:id id=... 50 | fmt.Fprintf(w, "GET %s id=%s", routePath, id) 51 | }) 52 | 53 | http.ListenAndServe(":8080", router) 54 | ``` 55 | 56 | #### New Router with Context Support 57 | 58 | The `NewContextMux` function returns a router preconfigured for use with `context` and `http.HandlerFunc`. 59 | 60 | ```go 61 | router := httptreemux.NewContextMux() 62 | 63 | router.GET("/:page", func(w http.ResponseWriter, r *http.Request) { 64 | params := httptreemux.ContextParams(r.Context()) 65 | fmt.Fprintf(w, "GET /%s", params["page"]) 66 | }) 67 | 68 | group := router.NewGroup("/api") 69 | group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request) { 70 | ctxData := httptreemux.ContextData(r.Context()) 71 | params := ctxData.Params() 72 | id := params["id"] 73 | 74 | // Useful for middleware to see which route was hit without dealing with wildcards 75 | routePath := ctxData.Route() 76 | 77 | // Prints GET /api/v1/:id id=... 78 | fmt.Fprintf(w, "GET %s id=%s", routePath, id) 79 | }) 80 | 81 | http.ListenAndServe(":8080", router) 82 | ``` 83 | 84 | 85 | 86 | ## Routing Rules 87 | The syntax here is also modeled after httprouter. Each variable in a path may match on one segment only, except for an optional catch-all variable at the end of the URL. 88 | 89 | Some examples of valid URL patterns are: 90 | * `/post/all` 91 | * `/post/:postid` 92 | * `/post/:postid/page/:page` 93 | * `/post/:postid/:page` 94 | * `/images/*path` 95 | * `/favicon.ico` 96 | * `/:year/:month/` 97 | * `/:year/:month/:post` 98 | * `/:page` 99 | 100 | Note that all of the above URL patterns may exist concurrently in the router. 101 | 102 | Path elements starting with `:` indicate a wildcard in the path. A wildcard will only match on a single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, but not `/post/1/2`. 103 | 104 | A path element starting with `*` is a catch-all, whose value will be a string containing all text in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a requested URL `images/abc/def`, path would contain `abc/def`. A catch-all path will not match an empty string, so in this example a separate route would need to be installed if you also want to match `/images/`. 105 | 106 | #### Using : and * in routing patterns 107 | 108 | The characters `:` and `*` can be used at the beginning of a path segment by escaping them with a backslash. A double backslash at the beginning of a segment is interpreted as a single backslash. These escapes are only checked at the very beginning of a path segment; they are not necessary or processed elsewhere in a token. 109 | 110 | ```go 111 | router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken 112 | router.GET("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle 113 | router.GET("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\* 114 | router.GET("/foo/\\\\*backslashWithStar") // matches /foo/\*backslashWithStar 115 | ``` 116 | 117 | ### Routing Groups 118 | Lets you create a new group of routes with a given path prefix. Makes it easier to create clusters of paths like: 119 | * `/api/v1/foo` 120 | * `/api/v1/bar` 121 | 122 | To use this you do: 123 | ```go 124 | router = httptreemux.New() 125 | api := router.NewGroup("/api/v1") 126 | api.GET("/foo", fooHandler) // becomes /api/v1/foo 127 | api.GET("/bar", barHandler) // becomes /api/v1/bar 128 | ``` 129 | 130 | ### Routing Priority 131 | The priority rules in the router are simple. 132 | 133 | 1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned. 134 | 2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL. 135 | 3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern. 136 | 137 | So with the following patterns adapted from [simpleblog](https://www.github.com/dimfeld/simpleblog), we'll see certain matches: 138 | ```go 139 | router = httptreemux.New() 140 | router.GET("/:page", pageHandler) 141 | router.GET("/:year/:month/:post", postHandler) 142 | router.GET("/:year/:month", archiveHandler) 143 | router.GET("/images/*path", staticHandler) 144 | router.GET("/favicon.ico", staticHandler) 145 | ``` 146 | 147 | #### Example scenarios 148 | 149 | - `/abc` will match `/:page` 150 | - `/2014/05` will match `/:year/:month` 151 | - `/2014/05/really-great-blog-post` will match `/:year/:month/:post` 152 | - `/images/CoolImage.gif` will match `/images/*path` 153 | - `/images/2014/05/MayImage.jpg` will also match `/images/*path`, with all the text after `/images` stored in the variable path. 154 | - `/favicon.ico` will match `/favicon.ico` 155 | 156 | ### Special Method Behavior 157 | If TreeMux.HeadCanUseGet is set to true, the router will call the GET handler for a pattern when a HEAD request is processed, if no HEAD handler has been added for that pattern. This behavior is enabled by default. 158 | 159 | Go's http.ServeContent and related functions already handle the HEAD method correctly by sending only the header, so in most cases your handlers will not need any special cases for it. 160 | 161 | By default TreeMux.OptionsHandler is a null handler that doesn't affect your routing. If you set the handler, it will be called on OPTIONS requests to a path already registered by another method. If you set a path specific handler by using `router.OPTIONS`, it will override the global Options Handler for that path. 162 | 163 | ### Trailing Slashes 164 | The router has special handling for paths with trailing slashes. If a pattern is added to the router with a trailing slash, any matches on that pattern without a trailing slash will be redirected to the version with the slash. If a pattern does not have a trailing slash, matches on that pattern with a trailing slash will be redirected to the version without. 165 | 166 | The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a method with a trailing slash, all other methods for that pattern will also be considered to have a trailing slash, regardless of whether or not it is specified for those methods too. 167 | However this behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By default it is set to true. 168 | 169 | One exception to this rule is catch-all patterns. By default, trailing slash redirection is disabled on catch-all patterns, since the structure of the entire URL and the desired patterns can not be predicted. If trailing slash removal is desired on catch-all patterns, set TreeMux.RemoveCatchAllTrailingSlash to true. 170 | 171 | ```go 172 | router = httptreemux.New() 173 | router.GET("/about", pageHandler) 174 | router.GET("/posts/", postIndexHandler) 175 | router.POST("/posts", postFormHandler) 176 | 177 | GET /about will match normally. 178 | GET /about/ will redirect to /about. 179 | GET /posts will redirect to /posts/. 180 | GET /posts/ will match normally. 181 | POST /posts will redirect to /posts/, because the GET method used a trailing slash. 182 | ``` 183 | 184 | ### Custom Redirects 185 | 186 | RedirectBehavior sets the behavior when the router redirects the request to the canonical version of the requested URL using RedirectTrailingSlash or RedirectClean. The default behavior is to return a 301 status, redirecting the browser to the version of the URL that matches the given pattern. 187 | 188 | These are the values accepted for RedirectBehavior. You may also add these values to the RedirectMethodBehavior map to define custom per-method redirect behavior. 189 | 190 | * Redirect301 - HTTP 301 Moved Permanently; this is the default. 191 | * Redirect307 - HTTP/1.1 Temporary Redirect 192 | * Redirect308 - RFC7538 Permanent Redirect 193 | * UseHandler - Don't redirect to the canonical path. Just call the handler instead. 194 | 195 | ### Case Insensitive Routing 196 | 197 | You can optionally allow case-insensitive routing by setting the _CaseInsensitive_ property on the router to true. 198 | This allows you to make all routes case-insensitive. For example: 199 | ```go 200 | router := httptreemux.New() 201 | router.CaseInsensitive 202 | router.GET("/My-RoUtE", pageHandler) 203 | ``` 204 | In this example, performing a GET request to /my-route will match the route and execute the _pageHandler_ functionality. 205 | It's important to note that when using case-insensitive routing, the CaseInsensitive property must be set before routes are defined or there may be unexpected side effects. 206 | 207 | #### Rationale/Usage 208 | On a POST request, most browsers that receive a 301 will submit a GET request to the redirected URL, meaning that any data will likely be lost. If you want to handle and avoid this behavior, you may use Redirect307, which causes most browsers to resubmit the request using the original method and request body. 209 | 210 | Since 307 is supposed to be a temporary redirect, the new 308 status code has been proposed, which is treated the same, except it indicates correctly that the redirection is permanent. The big caveat here is that the RFC is relatively recent, and older or non-compliant browsers will not handle it. Therefore its use is not recommended unless you really know what you're doing. 211 | 212 | Finally, the UseHandler value will simply call the handler function for the pattern, without redirecting to the canonical version of the URL. 213 | 214 | ### RequestURI vs. URL.Path 215 | 216 | #### Escaped Slashes 217 | Go automatically processes escaped characters in a URL, converting + to a space and %XX to the corresponding character. This can present issues when the URL contains a %2f, which is unescaped to '/'. This isn't an issue for most applications, but it will prevent the router from correctly matching paths and wildcards. 218 | 219 | For example, the pattern `/post/:post` would not match on `/post/abc%2fdef`, which is unescaped to `/post/abc/def`. The desired behavior is that it matches, and the `post` wildcard is set to `abc/def`. 220 | 221 | Therefore, this router defaults to using the raw URL, stored in the Request.RequestURI variable. Matching wildcards and catch-alls are then unescaped, to give the desired behavior. 222 | 223 | TL;DR: If a requested URL contains a %2f, this router will still do the right thing. Some Go HTTP routers may not due to [Go issue 3659](https://code.google.com/p/go/issues/detail?id=3659). 224 | 225 | #### Escaped Characters 226 | 227 | As mentioned above, characters in the URL are not unescaped when using RequestURI to determine the matched route. If this is a problem for you and you are unable to switch to URL.Path for the above reasons, you may set `router.EscapeAddedRoutes` to `true`. This option will run each added route through the `URL.EscapedPath` function, and add an additional route if the escaped version differs. 228 | 229 | #### http Package Utility Functions 230 | 231 | Although using RequestURI avoids the issue described above, certain utility functions such as `http.StripPrefix` modify URL.Path, and expect that the underlying router is using that field to make its decision. If you are using some of these functions, set the router's `PathSource` member to `URLPath`. This will give up the proper handling of escaped slashes described above, while allowing the router to work properly with these utility functions. 232 | 233 | ## Concurrency 234 | 235 | The router contains an `RWMutex` that arbitrates access to the tree. This allows routes to be safely added from multiple goroutines at once. 236 | 237 | No concurrency controls are needed when only reading from the tree, so the default behavior is to not use the `RWMutex` when serving a request. This avoids a theoretical slowdown under high-usage scenarios from competing atomic integer operations inside the `RWMutex`. If your application adds routes to the router after it has begun serving requests, you should avoid potential race conditions by setting `router.SafeAddRoutesWhileRunning` to `true` to use the `RWMutex` when serving requests. 238 | 239 | ## Error Handlers 240 | 241 | ### NotFoundHandler 242 | TreeMux.NotFoundHandler can be set to provide custom 404-error handling. The default implementation is Go's `http.NotFound` function. 243 | 244 | ### MethodNotAllowedHandler 245 | If a pattern matches, but the pattern does not have an associated handler for the requested method, the router calls the MethodNotAllowedHandler. The default 246 | version of this handler just writes the status code `http.StatusMethodNotAllowed` and sets the response header's `Allowed` field appropriately. 247 | 248 | ### Panic Handling 249 | TreeMux.PanicHandler can be set to provide custom panic handling. The `SimplePanicHandler` just writes the status code `http.StatusInternalServerError`. The function `ShowErrorsPanicHandler`, adapted from [gocraft/web](https://github.com/gocraft/web), will print panic errors to the browser in an easily-readable format. 250 | 251 | ## Unexpected Differences from Other Routers 252 | 253 | This router is intentionally light on features in the name of simplicity and 254 | performance. When coming from another router that does heavier processing behind 255 | the scenes, you may encounter some unexpected behavior. This list is by no means 256 | exhaustive, but covers some nonobvious cases that users have encountered. 257 | 258 | ### gorilla/pat query string modifications 259 | 260 | When matching on parameters in a route, the `gorilla/pat` router will modify 261 | `Request.URL.RawQuery` to make it appear like the parameters were in the 262 | query string. `httptreemux` does not do this. See [Issue #26](https://github.com/dimfeld/httptreemux/issues/26) for more details and a 263 | code snippet that can perform this transformation for you, should you want it. 264 | 265 | ### httprouter and catch-all parameters 266 | 267 | When using `httprouter`, a route with a catch-all parameter (e.g. `/images/*path`) will match on URLs like `/images/` where the catch-all parameter is empty. This router does not match on empty catch-all parameters, but the behavior can be duplicated by adding a route without the catch-all (e.g. `/images/`). 268 | 269 | ## Middleware 270 | This package provides no middleware. But there are a lot of great options out there and it's pretty easy to write your own. The router provides the `Use` and `UseHandler` functions to ease the creation of middleware chains. (Real documentation of these functions coming soon.) 271 | 272 | # Acknowledgements 273 | 274 | * Inspiration from Julien Schmidt's [httprouter](https://github.com/julienschmidt/httprouter) 275 | * Show Errors panic handler from [gocraft/web](https://github.com/gocraft/web) 276 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package httptreemux 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "reflect" 11 | "sort" 12 | "strings" 13 | "sync" 14 | "testing" 15 | ) 16 | 17 | func simpleHandler(w http.ResponseWriter, r *http.Request, params map[string]string) {} 18 | 19 | func panicHandler(w http.ResponseWriter, r *http.Request, params map[string]string) { 20 | panic("test panic") 21 | } 22 | 23 | func newRequest(method, path string, body io.Reader) (*http.Request, error) { 24 | r, _ := http.NewRequest(method, path, body) 25 | u, _ := url.ParseRequestURI(path) 26 | r.URL = u 27 | r.RequestURI = path 28 | return r, nil 29 | } 30 | 31 | type RequestCreator func(string, string, io.Reader) (*http.Request, error) 32 | type TestScenario struct { 33 | RequestCreator RequestCreator 34 | ServeStyle bool 35 | description string 36 | } 37 | 38 | var scenarios = []TestScenario{ 39 | TestScenario{newRequest, false, "Test with RequestURI and normal ServeHTTP"}, 40 | TestScenario{http.NewRequest, false, "Test with URL.Path and normal ServeHTTP"}, 41 | TestScenario{newRequest, true, "Test with RequestURI and LookupResult"}, 42 | TestScenario{http.NewRequest, true, "Test with URL.Path and LookupResult"}, 43 | } 44 | 45 | // This type and the benchRequest function are modified from go-http-routing-benchmark. 46 | type mockResponseWriter struct { 47 | code int 48 | calledWrite bool 49 | } 50 | 51 | func (m *mockResponseWriter) Header() (h http.Header) { 52 | return http.Header{} 53 | } 54 | 55 | func (m *mockResponseWriter) Write(p []byte) (n int, err error) { 56 | m.calledWrite = true 57 | return len(p), nil 58 | } 59 | 60 | func (m *mockResponseWriter) WriteString(s string) (n int, err error) { 61 | m.calledWrite = true 62 | return len(s), nil 63 | } 64 | 65 | func (m *mockResponseWriter) WriteHeader(code int) { 66 | m.code = code 67 | } 68 | 69 | func benchRequest(b *testing.B, router http.Handler, r *http.Request) { 70 | w := new(mockResponseWriter) 71 | 72 | b.ReportAllocs() 73 | b.ResetTimer() 74 | 75 | for i := 0; i < b.N; i++ { 76 | router.ServeHTTP(w, r) 77 | } 78 | } 79 | 80 | func serve(router *TreeMux, w http.ResponseWriter, r *http.Request, useLookup bool) bool { 81 | if useLookup { 82 | result, found := router.Lookup(w, r) 83 | router.ServeLookupResult(w, r, result) 84 | return found 85 | } else { 86 | router.ServeHTTP(w, r) 87 | return true 88 | } 89 | } 90 | 91 | func TestMethods(t *testing.T) { 92 | for _, scenario := range scenarios { 93 | t.Log(scenario.description) 94 | testMethods(t, scenario.RequestCreator, true, scenario.ServeStyle) 95 | testMethods(t, scenario.RequestCreator, false, scenario.ServeStyle) 96 | } 97 | } 98 | 99 | func testMethods(t *testing.T, newRequest RequestCreator, headCanUseGet bool, useSeparateLookup bool) { 100 | var result string 101 | 102 | makeHandler := func(method string) HandlerFunc { 103 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 104 | result = method 105 | } 106 | } 107 | 108 | router := New() 109 | router.HeadCanUseGet = headCanUseGet 110 | router.GET("/user/:param", makeHandler("GET")) 111 | router.POST("/user/:param", makeHandler("POST")) 112 | router.PATCH("/user/:param", makeHandler("PATCH")) 113 | router.PUT("/user/:param", makeHandler("PUT")) 114 | router.DELETE("/user/:param", makeHandler("DELETE")) 115 | 116 | testMethod := func(method, expect string) { 117 | result = "" 118 | w := httptest.NewRecorder() 119 | r, _ := newRequest(method, "/user/"+method, nil) 120 | found := serve(router, w, r, useSeparateLookup) 121 | 122 | if useSeparateLookup && expect == "" && found { 123 | t.Errorf("Lookup unexpectedly succeeded for method %s", method) 124 | } 125 | 126 | if expect == "" && w.Code != http.StatusMethodNotAllowed { 127 | t.Errorf("Method %s not expected to match but saw code %d", method, w.Code) 128 | } 129 | 130 | if result != expect { 131 | t.Errorf("Method %s got result %s", method, result) 132 | } 133 | } 134 | 135 | testMethod("GET", "GET") 136 | testMethod("POST", "POST") 137 | testMethod("PATCH", "PATCH") 138 | testMethod("PUT", "PUT") 139 | testMethod("DELETE", "DELETE") 140 | if headCanUseGet { 141 | t.Log("Test implicit HEAD with HeadCanUseGet = true") 142 | testMethod("HEAD", "GET") 143 | } else { 144 | t.Log("Test implicit HEAD with HeadCanUseGet = false") 145 | testMethod("HEAD", "") 146 | } 147 | 148 | router.HEAD("/user/:param", makeHandler("HEAD")) 149 | testMethod("HEAD", "HEAD") 150 | } 151 | 152 | func TestCaseInsensitiveRouting(t *testing.T) { 153 | router := New() 154 | // create case-insensitive route 155 | router.CaseInsensitive = true 156 | router.GET("/MY-path", simpleHandler) 157 | 158 | w := httptest.NewRecorder() 159 | r, _ := newRequest("GET", "/MY-PATH", nil) 160 | router.ServeHTTP(w, r) 161 | 162 | w = httptest.NewRecorder() 163 | router.ServeHTTP(w, r) 164 | if w.Code != http.StatusOK { 165 | t.Errorf("expected 200 response for case-insensitive request. Received: %d", w.Code) 166 | } 167 | } 168 | 169 | func TestNotFound(t *testing.T) { 170 | calledNotFound := false 171 | 172 | notFoundHandler := func(w http.ResponseWriter, r *http.Request) { 173 | calledNotFound = true 174 | } 175 | 176 | router := New() 177 | router.GET("/user/abc", simpleHandler) 178 | 179 | w := httptest.NewRecorder() 180 | r, _ := newRequest("GET", "/abc/", nil) 181 | router.ServeHTTP(w, r) 182 | 183 | if w.Code != http.StatusNotFound { 184 | t.Errorf("Expected error 404 from built-in not found handler but saw %d", w.Code) 185 | } 186 | 187 | // Now try with a custome handler. 188 | router.NotFoundHandler = notFoundHandler 189 | 190 | router.ServeHTTP(w, r) 191 | if !calledNotFound { 192 | t.Error("Custom not found handler was not called") 193 | } 194 | } 195 | 196 | func TestMethodNotAllowedHandler(t *testing.T) { 197 | calledNotAllowed := false 198 | 199 | notAllowedHandler := func(w http.ResponseWriter, r *http.Request, 200 | methods map[string]HandlerFunc) { 201 | 202 | calledNotAllowed = true 203 | 204 | expected := []string{"GET", "PUT", "DELETE", "HEAD"} 205 | allowed := make([]string, 0) 206 | for m := range methods { 207 | allowed = append(allowed, m) 208 | } 209 | 210 | sort.Strings(expected) 211 | sort.Strings(allowed) 212 | 213 | if !reflect.DeepEqual(expected, allowed) { 214 | t.Errorf("Custom handler expected map %v, saw %v", 215 | expected, allowed) 216 | } 217 | } 218 | 219 | router := New() 220 | router.GET("/user/abc", simpleHandler) 221 | router.PUT("/user/abc", simpleHandler) 222 | router.DELETE("/user/abc", simpleHandler) 223 | 224 | w := httptest.NewRecorder() 225 | r, _ := newRequest("POST", "/user/abc", nil) 226 | router.ServeHTTP(w, r) 227 | 228 | if w.Code != http.StatusMethodNotAllowed { 229 | t.Errorf("Expected error %d from built-in not found handler but saw %d", 230 | http.StatusMethodNotAllowed, w.Code) 231 | } 232 | 233 | allowed := w.Header()["Allow"] 234 | sort.Strings(allowed) 235 | expected := []string{"DELETE", "GET", "PUT", "HEAD"} 236 | sort.Strings(expected) 237 | 238 | if !reflect.DeepEqual(allowed, expected) { 239 | t.Errorf("Expected Allow header %v, saw %v", 240 | expected, allowed) 241 | } 242 | 243 | // Now try with a custom handler. 244 | router.MethodNotAllowedHandler = notAllowedHandler 245 | 246 | router.ServeHTTP(w, r) 247 | if !calledNotAllowed { 248 | t.Error("Custom not allowed handler was not called") 249 | } 250 | } 251 | 252 | func TestOptionsHandler(t *testing.T) { 253 | optionsHandler := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { 254 | w.Header().Set("Access-Control-Allow-Origin", "*") 255 | w.WriteHeader(http.StatusNoContent) 256 | } 257 | 258 | customOptionsHandler := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { 259 | w.Header().Set("Access-Control-Allow-Origin", "httptreemux.com") 260 | w.WriteHeader(http.StatusUnauthorized) 261 | } 262 | 263 | router := New() 264 | router.GET("/user/abc", simpleHandler) 265 | router.PUT("/user/abc", simpleHandler) 266 | router.DELETE("/user/abc", simpleHandler) 267 | router.OPTIONS("/user/abc/options", customOptionsHandler) 268 | 269 | // test without an OPTIONS handler 270 | w := httptest.NewRecorder() 271 | r, _ := newRequest("OPTIONS", "/user/abc", nil) 272 | router.ServeHTTP(w, r) 273 | 274 | if w.Code != http.StatusMethodNotAllowed { 275 | t.Errorf("Expected error %d from built-in not found handler but saw %d", 276 | http.StatusMethodNotAllowed, w.Code) 277 | } 278 | 279 | // Now try with a global options handler. 280 | router.OptionsHandler = optionsHandler 281 | 282 | w = httptest.NewRecorder() 283 | router.ServeHTTP(w, r) 284 | if !(w.Code == http.StatusNoContent && w.Header()["Access-Control-Allow-Origin"][0] == "*") { 285 | t.Error("global options handler was not called") 286 | } 287 | 288 | // Now see if a custom handler overwrites the global options handler. 289 | w = httptest.NewRecorder() 290 | r, _ = newRequest("OPTIONS", "/user/abc/options", nil) 291 | router.ServeHTTP(w, r) 292 | if !(w.Code == http.StatusUnauthorized && w.Header()["Access-Control-Allow-Origin"][0] == "httptreemux.com") { 293 | t.Error("custom options handler did not overwrite global handler") 294 | } 295 | 296 | // Now see if a custom handler works with the global options handler set to nil. 297 | router.OptionsHandler = nil 298 | w = httptest.NewRecorder() 299 | r, _ = newRequest("OPTIONS", "/user/abc/options", nil) 300 | router.ServeHTTP(w, r) 301 | if !(w.Code == http.StatusUnauthorized && w.Header()["Access-Control-Allow-Origin"][0] == "httptreemux.com") { 302 | t.Error("custom options handler did not overwrite global handler") 303 | } 304 | 305 | // Make sure that the MethodNotAllowedHandler works when OptionsHandler is set 306 | router.OptionsHandler = optionsHandler 307 | w = httptest.NewRecorder() 308 | r, _ = newRequest("POST", "/user/abc", nil) 309 | router.ServeHTTP(w, r) 310 | 311 | if w.Code != http.StatusMethodNotAllowed { 312 | t.Errorf("Expected error %d from built-in not found handler but saw %d", 313 | http.StatusMethodNotAllowed, w.Code) 314 | } 315 | 316 | allowed := w.Header()["Allow"] 317 | sort.Strings(allowed) 318 | expected := []string{"DELETE", "GET", "PUT", "HEAD"} 319 | sort.Strings(expected) 320 | 321 | if !reflect.DeepEqual(allowed, expected) { 322 | t.Errorf("Expected Allow header %v, saw %v", 323 | expected, allowed) 324 | } 325 | } 326 | 327 | func TestPanic(t *testing.T) { 328 | 329 | router := New() 330 | router.PanicHandler = SimplePanicHandler 331 | router.GET("/abc", panicHandler) 332 | r, _ := newRequest("GET", "/abc", nil) 333 | w := httptest.NewRecorder() 334 | 335 | router.ServeHTTP(w, r) 336 | 337 | if w.Code != http.StatusInternalServerError { 338 | t.Errorf("Expected code %d from default panic handler, saw %d", 339 | http.StatusInternalServerError, w.Code) 340 | } 341 | 342 | sawPanic := false 343 | router.PanicHandler = func(w http.ResponseWriter, r *http.Request, err interface{}) { 344 | sawPanic = true 345 | } 346 | 347 | router.ServeHTTP(w, r) 348 | if !sawPanic { 349 | t.Errorf("Custom panic handler was not called") 350 | } 351 | 352 | // Assume this does the right thing. Just a sanity test. 353 | router.PanicHandler = ShowErrorsPanicHandler 354 | w = httptest.NewRecorder() 355 | router.ServeHTTP(w, r) 356 | if w.Code != http.StatusInternalServerError { 357 | t.Errorf("Expected code %d from ShowErrorsPanicHandler, saw %d", 358 | http.StatusInternalServerError, w.Code) 359 | } 360 | } 361 | 362 | func TestRedirect(t *testing.T) { 363 | for _, scenario := range scenarios { 364 | t.Log(scenario.description) 365 | t.Log("Testing with all 301") 366 | testRedirect(t, Redirect301, Redirect301, Redirect301, false, scenario.RequestCreator, scenario.ServeStyle) 367 | t.Log("Testing with all UseHandler") 368 | testRedirect(t, UseHandler, UseHandler, UseHandler, false, scenario.RequestCreator, scenario.ServeStyle) 369 | t.Log("Testing with default 301, GET 307, POST UseHandler") 370 | testRedirect(t, Redirect301, Redirect307, UseHandler, true, scenario.RequestCreator, scenario.ServeStyle) 371 | t.Log("Testing with default UseHandler, GET 301, POST 308") 372 | testRedirect(t, UseHandler, Redirect301, Redirect308, true, scenario.RequestCreator, scenario.ServeStyle) 373 | } 374 | } 375 | 376 | func behaviorToCode(b RedirectBehavior) int { 377 | switch b { 378 | case Redirect301: 379 | return http.StatusMovedPermanently 380 | case Redirect307: 381 | return http.StatusTemporaryRedirect 382 | case Redirect308: 383 | return 308 384 | case UseHandler: 385 | // Not normally, but the handler in the below test returns this. 386 | return http.StatusNoContent 387 | } 388 | 389 | panic("Unhandled behavior!") 390 | } 391 | 392 | func testRedirect(t *testing.T, defaultBehavior, getBehavior, postBehavior RedirectBehavior, customMethods bool, 393 | newRequest RequestCreator, serveStyle bool) { 394 | 395 | var redirHandler = func(w http.ResponseWriter, r *http.Request, params map[string]string) { 396 | // Returning this instead of 200 makes it easy to verify that the handler is actually getting called. 397 | w.WriteHeader(http.StatusNoContent) 398 | } 399 | 400 | router := New() 401 | router.RedirectBehavior = defaultBehavior 402 | 403 | var expectedCodeMap = map[string]int{"PUT": behaviorToCode(defaultBehavior)} 404 | 405 | if customMethods { 406 | router.RedirectMethodBehavior["GET"] = getBehavior 407 | router.RedirectMethodBehavior["POST"] = postBehavior 408 | expectedCodeMap["GET"] = behaviorToCode(getBehavior) 409 | expectedCodeMap["POST"] = behaviorToCode(postBehavior) 410 | } else { 411 | expectedCodeMap["GET"] = expectedCodeMap["PUT"] 412 | expectedCodeMap["POST"] = expectedCodeMap["PUT"] 413 | } 414 | 415 | router.GET("/slash/", redirHandler) 416 | router.GET("/noslash", redirHandler) 417 | router.POST("/slash/", redirHandler) 418 | router.POST("/noslash", redirHandler) 419 | router.PUT("/slash/", redirHandler) 420 | router.PUT("/noslash", redirHandler) 421 | 422 | for method, expectedCode := range expectedCodeMap { 423 | t.Logf("Testing method %s, expecting code %d", method, expectedCode) 424 | 425 | w := httptest.NewRecorder() 426 | r, _ := newRequest(method, "/slash", nil) 427 | found := serve(router, w, r, serveStyle) 428 | if found == false { 429 | t.Errorf("/slash: found returned false") 430 | } 431 | if w.Code != expectedCode { 432 | t.Errorf("/slash expected code %d, saw %d", expectedCode, w.Code) 433 | } 434 | if expectedCode != http.StatusNoContent && w.Header().Get("Location") != "/slash/" { 435 | t.Errorf("/slash was not redirected to /slash/") 436 | } 437 | 438 | r, _ = newRequest(method, "/noslash/", nil) 439 | w = httptest.NewRecorder() 440 | found = serve(router, w, r, serveStyle) 441 | if found == false { 442 | t.Errorf("/noslash: found returned false") 443 | } 444 | if w.Code != expectedCode { 445 | t.Errorf("/noslash/ expected code %d, saw %d", expectedCode, w.Code) 446 | } 447 | if expectedCode != http.StatusNoContent && w.Header().Get("Location") != "/noslash" { 448 | t.Errorf("/noslash/ was redirected to `%s` instead of /noslash", w.Header().Get("Location")) 449 | } 450 | 451 | r, _ = newRequest(method, "//noslash/", nil) 452 | if r.RequestURI == "//noslash/" { // http.NewRequest parses this out differently 453 | w = httptest.NewRecorder() 454 | found = serve(router, w, r, serveStyle) 455 | if found == false { 456 | t.Errorf("//noslash/: found returned false") 457 | } 458 | if w.Code != expectedCode { 459 | t.Errorf("//noslash/ expected code %d, saw %d", expectedCode, w.Code) 460 | } 461 | if expectedCode != http.StatusNoContent && w.Header().Get("Location") != "/noslash" { 462 | t.Errorf("//noslash/ was redirected to %s, expected /noslash", w.Header().Get("Location")) 463 | } 464 | } 465 | 466 | // Test nonredirect cases 467 | r, _ = newRequest(method, "/noslash", nil) 468 | w = httptest.NewRecorder() 469 | found = serve(router, w, r, serveStyle) 470 | if found == false { 471 | t.Errorf("/noslash (non-redirect): found returned false") 472 | } 473 | if w.Code != http.StatusNoContent { 474 | t.Errorf("/noslash (non-redirect) expected code %d, saw %d", http.StatusNoContent, w.Code) 475 | } 476 | 477 | r, _ = newRequest(method, "/noslash?a=1&b=2", nil) 478 | w = httptest.NewRecorder() 479 | found = serve(router, w, r, serveStyle) 480 | if found == false { 481 | t.Errorf("/noslash (non-redirect): found returned false") 482 | } 483 | if w.Code != http.StatusNoContent { 484 | t.Errorf("/noslash (non-redirect) expected code %d, saw %d", http.StatusNoContent, w.Code) 485 | } 486 | 487 | r, _ = newRequest(method, "/slash/", nil) 488 | w = httptest.NewRecorder() 489 | found = serve(router, w, r, serveStyle) 490 | if found == false { 491 | t.Errorf("/slash/ (non-redirect): found returned false") 492 | } 493 | if w.Code != http.StatusNoContent { 494 | t.Errorf("/slash/ (non-redirect) expected code %d, saw %d", http.StatusNoContent, w.Code) 495 | } 496 | 497 | r, _ = newRequest(method, "/slash/?a=1&b=2", nil) 498 | w = httptest.NewRecorder() 499 | found = serve(router, w, r, serveStyle) 500 | if found == false { 501 | t.Errorf("/slash/?a=1&b=2: found returned false") 502 | } 503 | if w.Code != http.StatusNoContent { 504 | t.Errorf("/slash/?a=1&b=2 expected code %d, saw %d", http.StatusNoContent, w.Code) 505 | } 506 | 507 | // Test querystring and fragment cases 508 | r, _ = newRequest(method, "/slash?a=1&b=2", nil) 509 | w = httptest.NewRecorder() 510 | found = serve(router, w, r, serveStyle) 511 | if found == false { 512 | t.Errorf("/slash?a=1&b=2 : found returned false") 513 | } 514 | if w.Code != expectedCode { 515 | t.Errorf("/slash?a=1&b=2 expected code %d, saw %d", expectedCode, w.Code) 516 | } 517 | if expectedCode != http.StatusNoContent && w.Header().Get("Location") != "/slash/?a=1&b=2" { 518 | t.Errorf("/slash?a=1&b=2 was redirected to %s", w.Header().Get("Location")) 519 | } 520 | 521 | r, _ = newRequest(method, "/noslash/?a=1&b=2", nil) 522 | w = httptest.NewRecorder() 523 | found = serve(router, w, r, serveStyle) 524 | if found == false { 525 | t.Errorf("/noslash/?a=1&b=2: found returned false") 526 | } 527 | if w.Code != expectedCode { 528 | t.Errorf("/noslash/?a=1&b=2 expected code %d, saw %d", expectedCode, w.Code) 529 | } 530 | if expectedCode != http.StatusNoContent && w.Header().Get("Location") != "/noslash?a=1&b=2" { 531 | t.Errorf("/noslash/?a=1&b=2 was redirected to %s", w.Header().Get("Location")) 532 | } 533 | } 534 | } 535 | 536 | func TestSkipRedirect(t *testing.T) { 537 | router := New() 538 | router.RedirectTrailingSlash = false 539 | router.RedirectCleanPath = false 540 | router.GET("/slash/", simpleHandler) 541 | router.GET("/noslash", simpleHandler) 542 | 543 | w := httptest.NewRecorder() 544 | r, _ := newRequest("GET", "/slash", nil) 545 | router.ServeHTTP(w, r) 546 | if w.Code != http.StatusNotFound { 547 | t.Errorf("/slash expected code 404, saw %d", w.Code) 548 | } 549 | 550 | r, _ = newRequest("GET", "/noslash/", nil) 551 | w = httptest.NewRecorder() 552 | router.ServeHTTP(w, r) 553 | if w.Code != http.StatusNotFound { 554 | t.Errorf("/noslash/ expected code 404, saw %d", w.Code) 555 | } 556 | 557 | r, _ = newRequest("GET", "//noslash", nil) 558 | w = httptest.NewRecorder() 559 | router.ServeHTTP(w, r) 560 | if w.Code != http.StatusNotFound { 561 | t.Errorf("//noslash expected code 404, saw %d", w.Code) 562 | } 563 | } 564 | 565 | func TestCatchAllTrailingSlashRedirect(t *testing.T) { 566 | router := New() 567 | redirectSettings := []bool{false, true} 568 | 569 | router.GET("/abc/*path", simpleHandler) 570 | 571 | testPath := func(path string) { 572 | r, _ := newRequest("GET", "/abc/"+path, nil) 573 | w := httptest.NewRecorder() 574 | router.ServeHTTP(w, r) 575 | 576 | endingSlash := strings.HasSuffix(path, "/") 577 | 578 | var expectedCode int 579 | if endingSlash && router.RedirectTrailingSlash && router.RemoveCatchAllTrailingSlash { 580 | expectedCode = http.StatusMovedPermanently 581 | } else { 582 | expectedCode = http.StatusOK 583 | } 584 | 585 | if w.Code != expectedCode { 586 | t.Errorf("Path %s with RedirectTrailingSlash %v, RemoveCatchAllTrailingSlash %v "+ 587 | " expected code %d but saw %d", path, 588 | router.RedirectTrailingSlash, router.RemoveCatchAllTrailingSlash, 589 | expectedCode, w.Code) 590 | } 591 | } 592 | 593 | for _, redirectSetting := range redirectSettings { 594 | for _, removeCatchAllSlash := range redirectSettings { 595 | router.RemoveCatchAllTrailingSlash = removeCatchAllSlash 596 | router.RedirectTrailingSlash = redirectSetting 597 | 598 | testPath("apples") 599 | testPath("apples/") 600 | testPath("apples/bananas") 601 | testPath("apples/bananas/") 602 | } 603 | } 604 | 605 | } 606 | 607 | func TestRoot(t *testing.T) { 608 | for _, scenario := range scenarios { 609 | t.Log(scenario.description) 610 | handlerCalled := false 611 | handler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 612 | handlerCalled = true 613 | } 614 | router := New() 615 | router.GET("/", handler) 616 | 617 | r, _ := scenario.RequestCreator("GET", "/", nil) 618 | w := new(mockResponseWriter) 619 | serve(router, w, r, scenario.ServeStyle) 620 | 621 | if !handlerCalled { 622 | t.Error("Handler not called for root path") 623 | } 624 | } 625 | } 626 | 627 | func TestWildcardAtSplitNode(t *testing.T) { 628 | var suppliedParam string 629 | simpleHandler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 630 | t.Log(params) 631 | suppliedParam, _ = params["slug"] 632 | } 633 | 634 | router := New() 635 | router.GET("/pumpkin", simpleHandler) 636 | router.GET("/passing", simpleHandler) 637 | router.GET("/:slug", simpleHandler) 638 | router.GET("/:slug/abc", simpleHandler) 639 | 640 | t.Log(router.root.dumpTree("", " ")) 641 | 642 | r, _ := newRequest("GET", "/patch", nil) 643 | w := httptest.NewRecorder() 644 | router.ServeHTTP(w, r) 645 | 646 | if suppliedParam != "patch" { 647 | t.Errorf("Expected param patch, saw %s", suppliedParam) 648 | } 649 | 650 | if w.Code != http.StatusOK { 651 | t.Errorf("Expected status 200 for path /patch, saw %d", w.Code) 652 | } 653 | 654 | suppliedParam = "" 655 | r, _ = newRequest("GET", "/patch/abc", nil) 656 | w = httptest.NewRecorder() 657 | router.ServeHTTP(w, r) 658 | 659 | if suppliedParam != "patch" { 660 | t.Errorf("Expected param patch, saw %s", suppliedParam) 661 | } 662 | 663 | if w.Code != http.StatusOK { 664 | t.Errorf("Expected status 200 for path /patch/abc, saw %d", w.Code) 665 | } 666 | 667 | r, _ = newRequest("GET", "/patch/def", nil) 668 | w = httptest.NewRecorder() 669 | router.ServeHTTP(w, r) 670 | 671 | if w.Code != http.StatusNotFound { 672 | t.Errorf("Expected status 404 for path /patch/def, saw %d", w.Code) 673 | } 674 | } 675 | 676 | func TestSlash(t *testing.T) { 677 | param := "" 678 | handler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 679 | param = params["param"] 680 | } 681 | ymHandler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 682 | param = params["year"] + " " + params["month"] 683 | } 684 | router := New() 685 | router.GET("/abc/:param", handler) 686 | router.GET("/year/:year/month/:month", ymHandler) 687 | 688 | r, _ := newRequest("GET", "/abc/de%2ff", nil) 689 | w := new(mockResponseWriter) 690 | router.ServeHTTP(w, r) 691 | 692 | if param != "de/f" { 693 | t.Errorf("Expected param de/f, saw %s", param) 694 | } 695 | 696 | r, _ = newRequest("GET", "/year/de%2f/month/fg%2f", nil) 697 | router.ServeHTTP(w, r) 698 | 699 | if param != "de/ fg/" { 700 | t.Errorf("Expected param de/ fg/, saw %s", param) 701 | } 702 | } 703 | 704 | func TestQueryString(t *testing.T) { 705 | for _, scenario := range scenarios { 706 | t.Log(scenario.description) 707 | param := "" 708 | handler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 709 | param = params["param"] 710 | } 711 | router := New() 712 | router.GET("/static", handler) 713 | router.GET("/wildcard/:param", handler) 714 | router.GET("/catchall/*param", handler) 715 | 716 | r, _ := scenario.RequestCreator("GET", "/static?abc=def&ghi=jkl", nil) 717 | w := new(mockResponseWriter) 718 | 719 | param = "nomatch" 720 | serve(router, w, r, scenario.ServeStyle) 721 | if param != "" { 722 | t.Error("No match on", r.RequestURI) 723 | } 724 | 725 | r, _ = scenario.RequestCreator("GET", "/wildcard/aaa?abc=def", nil) 726 | serve(router, w, r, scenario.ServeStyle) 727 | if param != "aaa" { 728 | t.Error("Expected wildcard to match aaa, saw", param) 729 | } 730 | 731 | r, _ = scenario.RequestCreator("GET", "/catchall/bbb?abc=def", nil) 732 | serve(router, w, r, scenario.ServeStyle) 733 | if param != "bbb" { 734 | t.Error("Expected wildcard to match bbb, saw", param) 735 | } 736 | } 737 | } 738 | 739 | func TestPathSource(t *testing.T) { 740 | var called string 741 | 742 | appleHandler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 743 | called = "apples" 744 | } 745 | 746 | bananaHandler := func(w http.ResponseWriter, r *http.Request, params map[string]string) { 747 | called = "bananas" 748 | } 749 | router := New() 750 | router.GET("/apples", appleHandler) 751 | router.GET("/bananas", bananaHandler) 752 | 753 | // Set up a request with different values in URL and RequestURI. 754 | r, _ := newRequest("GET", "/apples", nil) 755 | r.RequestURI = "/bananas" 756 | w := new(mockResponseWriter) 757 | 758 | // Default setting should be RequestURI 759 | router.ServeHTTP(w, r) 760 | if called != "bananas" { 761 | t.Error("Using default, expected bananas but saw", called) 762 | } 763 | 764 | router.PathSource = URLPath 765 | router.ServeHTTP(w, r) 766 | if called != "apples" { 767 | t.Error("Using URLPath, expected apples but saw", called) 768 | } 769 | 770 | router.PathSource = RequestURI 771 | router.ServeHTTP(w, r) 772 | if called != "bananas" { 773 | t.Error("Using RequestURI, expected bananas but saw", called) 774 | } 775 | } 776 | 777 | func TestEscapedRoutes(t *testing.T) { 778 | type testcase struct { 779 | Route string 780 | Path string 781 | Param string 782 | ParamValue string 783 | } 784 | 785 | testcases := []*testcase{ 786 | {"/abc/def", "/abc/def", "", ""}, 787 | {"/abc/*star", "/abc/defg", "star", "defg"}, 788 | {"/abc/extrapath/*star", "/abc/extrapath/*lll", "star", "*lll"}, 789 | {"/abc/\\*def", "/abc/*def", "", ""}, 790 | {"/abc/\\\\*def", "/abc/\\*def", "", ""}, 791 | {"/:wild/def", "/*abcd/def", "wild", "*abcd"}, 792 | {"/\\:wild/def", "/:wild/def", "", ""}, 793 | {"/\\\\:wild/def", "/\\:wild/def", "", ""}, 794 | {"/\\*abc/def", "/*abc/def", "", ""}, 795 | } 796 | 797 | escapeCases := []bool{false, true} 798 | 799 | for _, escape := range escapeCases { 800 | var foundTestCase *testcase 801 | var foundParamKey string 802 | var foundParamValue string 803 | 804 | handleTestResponse := func(c *testcase, w http.ResponseWriter, r *http.Request, params map[string]string) { 805 | foundTestCase = c 806 | foundParamKey = "" 807 | foundParamValue = "" 808 | for key, val := range params { 809 | foundParamKey = key 810 | foundParamValue = val 811 | } 812 | t.Logf("RequestURI %s found test case %+v", r.RequestURI, c) 813 | } 814 | 815 | verify := func(c *testcase) { 816 | t.Logf("Expecting test case %+v", c) 817 | if c != foundTestCase { 818 | t.Errorf("Incorrectly matched test case %+v", foundTestCase) 819 | } 820 | 821 | if c.Param != foundParamKey { 822 | t.Errorf("Expected param key %s but saw %s", c.Param, foundParamKey) 823 | } 824 | 825 | if c.ParamValue != foundParamValue { 826 | t.Errorf("Expected param key %s but saw %s", c.Param, foundParamKey) 827 | } 828 | } 829 | 830 | t.Log("Recreating router") 831 | router := New() 832 | router.EscapeAddedRoutes = escape 833 | 834 | for _, c := range testcases { 835 | t.Logf("Adding route %s", c.Route) 836 | theCase := c 837 | router.GET(c.Route, func(w http.ResponseWriter, r *http.Request, params map[string]string) { 838 | handleTestResponse(theCase, w, r, params) 839 | }) 840 | } 841 | 842 | for _, c := range testcases { 843 | escapedPath := (&url.URL{Path: c.Path}).EscapedPath() 844 | escapedIsSame := escapedPath == c.Path 845 | 846 | r, _ := newRequest("GET", c.Path, nil) 847 | w := httptest.NewRecorder() 848 | router.ServeHTTP(w, r) 849 | if w.Code != 200 { 850 | t.Errorf("Escape %v test case %v saw code %d", escape, c, w.Code) 851 | } 852 | verify(c) 853 | 854 | if !escapedIsSame { 855 | r, _ := newRequest("GET", escapedPath, nil) 856 | w := httptest.NewRecorder() 857 | router.ServeHTTP(w, r) 858 | if router.EscapeAddedRoutes { 859 | // Expect a match 860 | if w.Code != 200 { 861 | t.Errorf("Escape %v test case %v saw code %d", escape, c, w.Code) 862 | } 863 | verify(c) 864 | } else { 865 | // Expect a non-match if the parameter isn't a wildcard. 866 | if foundParamKey == "" && w.Code != 404 { 867 | t.Errorf("Escape %v test case %v expected 404 saw %d", escape, c, w.Code) 868 | } 869 | } 870 | } 871 | } 872 | } 873 | } 874 | 875 | // Create a bunch of paths for testing. 876 | func createRoutes(numRoutes int) []string { 877 | letters := "abcdefghijhklmnopqrstuvwxyz" 878 | wordMap := map[string]bool{} 879 | for i := 0; i < numRoutes/2; i += 1 { 880 | length := (i % 4) + 4 881 | 882 | wordBytes := make([]byte, length) 883 | for charIndex := 0; charIndex < length; charIndex += 1 { 884 | wordBytes[charIndex] = letters[(i*3+charIndex*4)%len(letters)] 885 | } 886 | wordMap[string(wordBytes)] = true 887 | } 888 | 889 | words := make([]string, 0, len(wordMap)) 890 | for word := range wordMap { 891 | words = append(words, word) 892 | } 893 | 894 | routes := make([]string, 0, numRoutes) 895 | createdRoutes := map[string]bool{} 896 | rand.Seed(0) 897 | for len(routes) < numRoutes { 898 | first := words[rand.Int()%len(words)] 899 | second := words[rand.Int()%len(words)] 900 | third := words[rand.Int()%len(words)] 901 | route := fmt.Sprintf("/%s/%s/%s", first, second, third) 902 | 903 | if createdRoutes[route] { 904 | continue 905 | } 906 | createdRoutes[route] = true 907 | routes = append(routes, route) 908 | } 909 | 910 | return routes 911 | } 912 | 913 | // TestWriteConcurrency ensures that the router works with multiple goroutines adding 914 | // routes concurrently. 915 | func TestWriteConcurrency(t *testing.T) { 916 | router := New() 917 | 918 | // First create a bunch of routes 919 | numRoutes := 10000 920 | routes := createRoutes(numRoutes) 921 | 922 | wg := sync.WaitGroup{} 923 | addRoutes := func(base int, method string) { 924 | for i := 0; i < len(routes); i += 1 { 925 | route := routes[(i+base)%len(routes)] 926 | // t.Logf("Adding %s %s", method, route) 927 | router.Handle(method, route, simpleHandler) 928 | } 929 | wg.Done() 930 | } 931 | 932 | wg.Add(5) 933 | go addRoutes(100, "GET") 934 | go addRoutes(200, "POST") 935 | go addRoutes(300, "PATCH") 936 | go addRoutes(400, "PUT") 937 | go addRoutes(500, "DELETE") 938 | wg.Wait() 939 | 940 | handleRequests := func(method string) { 941 | for _, route := range routes { 942 | // t.Logf("Serving %s %s", method, route) 943 | r, _ := newRequest(method, route, nil) 944 | w := httptest.NewRecorder() 945 | router.ServeHTTP(w, r) 946 | 947 | if w.Code != 200 { 948 | t.Errorf("%s %s request failed", method, route) 949 | } 950 | } 951 | } 952 | 953 | handleRequests("GET") 954 | handleRequests("POST") 955 | handleRequests("PATCH") 956 | handleRequests("PUT") 957 | handleRequests("DELETE") 958 | } 959 | 960 | // TestReadWriteConcurrency ensures that when SafeAddRoutesWhileRunning is enabled, 961 | // the router is able to add routes while serving traffic. 962 | func TestReadWriteConcurrency(t *testing.T) { 963 | router := New() 964 | router.SafeAddRoutesWhileRunning = true 965 | 966 | // First create a bunch of routes 967 | numRoutes := 10000 968 | routes := createRoutes(numRoutes) 969 | 970 | wg := sync.WaitGroup{} 971 | addRoutes := func(base int, method string, routes []string) { 972 | for i := 0; i < len(routes); i += 1 { 973 | route := routes[(i+base)%len(routes)] 974 | // t.Logf("Adding %s %s", method, route) 975 | router.Handle(method, route, simpleHandler) 976 | } 977 | wg.Done() 978 | } 979 | 980 | handleRequests := func(base int, method string, routes []string, requireFound bool) { 981 | for i := 0; i < len(routes); i += 1 { 982 | route := routes[(i+base)%len(routes)] 983 | // t.Logf("Serving %s %s", method, route) 984 | r, _ := newRequest(method, route, nil) 985 | w := httptest.NewRecorder() 986 | router.ServeHTTP(w, r) 987 | 988 | if requireFound && w.Code != 200 { 989 | t.Errorf("%s %s request failed", method, route) 990 | } 991 | } 992 | wg.Done() 993 | } 994 | 995 | wg.Add(12) 996 | initialRoutes := routes[0 : numRoutes/10] 997 | addRoutes(0, "GET", initialRoutes) 998 | handleRequests(0, "GET", initialRoutes, true) 999 | 1000 | concurrentRoutes := routes[numRoutes/10:] 1001 | go addRoutes(100, "GET", concurrentRoutes) 1002 | go addRoutes(200, "POST", concurrentRoutes) 1003 | go addRoutes(300, "PATCH", concurrentRoutes) 1004 | go addRoutes(400, "PUT", concurrentRoutes) 1005 | go addRoutes(500, "DELETE", concurrentRoutes) 1006 | go handleRequests(50, "GET", routes, false) 1007 | go handleRequests(150, "POST", routes, false) 1008 | go handleRequests(250, "PATCH", routes, false) 1009 | go handleRequests(350, "PUT", routes, false) 1010 | go handleRequests(450, "DELETE", routes, false) 1011 | 1012 | wg.Wait() 1013 | 1014 | // Finally check all the routes and make sure they exist. 1015 | wg.Add(5) 1016 | handleRequests(0, "GET", routes, true) 1017 | handleRequests(0, "POST", concurrentRoutes, true) 1018 | handleRequests(0, "PATCH", concurrentRoutes, true) 1019 | handleRequests(0, "PUT", concurrentRoutes, true) 1020 | handleRequests(0, "DELETE", concurrentRoutes, true) 1021 | } 1022 | 1023 | func TestLookup(t *testing.T) { 1024 | router := New() 1025 | router.GET("/", simpleHandler) 1026 | router.GET("/user/dimfeld", simpleHandler) 1027 | router.POST("/user/dimfeld", simpleHandler) 1028 | router.GET("/abc/*", simpleHandler) 1029 | router.POST("/abc/*", simpleHandler) 1030 | 1031 | var tryLookup = func(method, path string, expectFound bool, expectCode int) { 1032 | r, _ := newRequest(method, path, nil) 1033 | w := &mockResponseWriter{} 1034 | lr, found := router.Lookup(w, r) 1035 | if found != expectFound { 1036 | t.Errorf("%s %s expected found %v, saw %v", method, path, expectFound, found) 1037 | } 1038 | 1039 | if lr.StatusCode != expectCode { 1040 | t.Errorf("%s %s expected status code %d, saw %d", method, path, expectCode, lr.StatusCode) 1041 | } 1042 | 1043 | if w.code != 0 { 1044 | t.Errorf("%s %s unexpectedly wrote status %d", method, path, w.code) 1045 | } 1046 | 1047 | if w.calledWrite { 1048 | t.Errorf("%s %s unexpectedly wrote data", method, path) 1049 | } 1050 | } 1051 | 1052 | tryLookup("GET", "/", true, http.StatusOK) 1053 | tryLookup("GET", "/", true, http.StatusOK) 1054 | tryLookup("POST", "/user/dimfeld", true, http.StatusOK) 1055 | tryLookup("POST", "/user/dimfeld/", true, http.StatusMovedPermanently) 1056 | tryLookup("PATCH", "/user/dimfeld", false, http.StatusMethodNotAllowed) 1057 | tryLookup("GET", "/abc/def/ghi", true, http.StatusOK) 1058 | 1059 | router.RedirectBehavior = Redirect307 1060 | tryLookup("POST", "/user/dimfeld/", true, http.StatusTemporaryRedirect) 1061 | } 1062 | 1063 | func TestRedirectEscapedPath(t *testing.T) { 1064 | router := New() 1065 | 1066 | testHandler := func(w http.ResponseWriter, r *http.Request, params map[string]string) {} 1067 | 1068 | router.GET("/:escaped/", testHandler) 1069 | 1070 | w := httptest.NewRecorder() 1071 | u, err := url.Parse("/Test P@th") 1072 | if err != nil { 1073 | t.Error(err) 1074 | return 1075 | } 1076 | 1077 | r, _ := newRequest("GET", u.String(), nil) 1078 | 1079 | router.ServeHTTP(w, r) 1080 | 1081 | if w.Code != http.StatusMovedPermanently { 1082 | t.Errorf("Expected status 301 but saw %d", w.Code) 1083 | } 1084 | 1085 | path := w.Header().Get("Location") 1086 | expected := "/Test%20P@th/" 1087 | if path != expected { 1088 | t.Errorf("Given path wasn't escaped correctly.\n"+ 1089 | "Expected: %q\nBut got: %q", expected, path) 1090 | } 1091 | } 1092 | 1093 | func TestMiddleware(t *testing.T) { 1094 | var execLog []string 1095 | 1096 | record := func(s string) { 1097 | execLog = append(execLog, s) 1098 | } 1099 | 1100 | assertExecLog := func(wanted []string) { 1101 | if !reflect.DeepEqual(execLog, wanted) { 1102 | t.Fatalf("got %v, wanted %v", execLog, wanted) 1103 | } 1104 | } 1105 | 1106 | newHandler := func(name string) HandlerFunc { 1107 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1108 | record(name) 1109 | } 1110 | } 1111 | 1112 | newMiddleware := func(name string) MiddlewareFunc { 1113 | return func(next HandlerFunc) HandlerFunc { 1114 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1115 | record(name) 1116 | next(w, r, params) 1117 | } 1118 | } 1119 | } 1120 | 1121 | newParamsMiddleware := func(name string, paramKey string) MiddlewareFunc { 1122 | return func(next HandlerFunc) HandlerFunc { 1123 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1124 | t.Log(params) 1125 | record(name) 1126 | record(params[paramKey]) 1127 | next(w, r, params) 1128 | } 1129 | } 1130 | } 1131 | 1132 | newHttpHandlerMiddleware := func(name string) func(http.Handler) http.Handler { 1133 | return func(next http.Handler) http.Handler { 1134 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1135 | record(name) 1136 | next.ServeHTTP(w, r) 1137 | }) 1138 | } 1139 | } 1140 | 1141 | router := New() 1142 | w := httptest.NewRecorder() 1143 | 1144 | t.Log("No middlewares.") 1145 | { 1146 | router.GET("/h1", newHandler("h1")) 1147 | 1148 | req, _ := newRequest("GET", "/h1", nil) 1149 | router.ServeHTTP(w, req) 1150 | 1151 | assertExecLog([]string{"h1"}) 1152 | } 1153 | 1154 | t.Log("Test route with and without middleware.") 1155 | { 1156 | execLog = nil 1157 | router.Use(newMiddleware("m1")) 1158 | router.GET("/h2", newHandler("h2")) 1159 | 1160 | req, _ := newRequest("GET", "/h1", nil) 1161 | router.ServeHTTP(w, req) 1162 | 1163 | req, _ = newRequest("GET", "/h2", nil) 1164 | router.ServeHTTP(w, req) 1165 | 1166 | assertExecLog([]string{"h1", "m1", "h2"}) 1167 | } 1168 | 1169 | t.Log("NewGroup inherits middlewares but has its own stack.") 1170 | { 1171 | execLog = nil 1172 | g := router.NewGroup("/g1") 1173 | g.Use(newMiddleware("m2")) 1174 | g.GET("/h3", newHandler("h3")) 1175 | 1176 | req, _ := newRequest("GET", "/h2", nil) 1177 | router.ServeHTTP(w, req) 1178 | 1179 | req, _ = newRequest("GET", "/g1/h3", nil) 1180 | router.ServeHTTP(w, req) 1181 | 1182 | assertExecLog([]string{"m1", "h2", "m1", "m2", "h3"}) 1183 | } 1184 | 1185 | t.Log("Middleware can modify params.") 1186 | { 1187 | execLog = nil 1188 | g := router.NewGroup("/g2") 1189 | g.Use(func(next HandlerFunc) HandlerFunc { 1190 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1191 | record("m4") 1192 | if params == nil { 1193 | params = make(map[string]string) 1194 | } 1195 | params["foo"] = "bar" 1196 | next(w, r, params) 1197 | } 1198 | }) 1199 | g.GET("/h6", func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1200 | record("h6") 1201 | if params["foo"] != "bar" { 1202 | t.Fatalf("got %q, wanted %q", params["foo"], "bar") 1203 | } 1204 | }) 1205 | 1206 | req, _ := newRequest("GET", "/g2/h6", nil) 1207 | router.ServeHTTP(w, req) 1208 | 1209 | assertExecLog([]string{"m1", "m4", "h6"}) 1210 | } 1211 | 1212 | t.Log("Middleware can serve request without calling next.") 1213 | { 1214 | execLog = nil 1215 | router.Use(func(_ HandlerFunc) HandlerFunc { 1216 | return func(w http.ResponseWriter, r *http.Request, params map[string]string) { 1217 | record("m3") 1218 | w.WriteHeader(http.StatusBadRequest) 1219 | w.Write([]byte("pong")) 1220 | } 1221 | }) 1222 | router.GET("/h5", newHandler("h5")) 1223 | 1224 | req, _ := newRequest("GET", "/h5", nil) 1225 | router.ServeHTTP(w, req) 1226 | 1227 | assertExecLog([]string{"m1", "m3"}) 1228 | if w.Code != http.StatusBadRequest { 1229 | t.Fatalf("got %d, wanted %d", w.Code, http.StatusBadRequest) 1230 | } 1231 | if w.Body.String() != "pong" { 1232 | t.Fatalf("got %s, wanted %s", w.Body.String(), "pong") 1233 | } 1234 | } 1235 | 1236 | t.Log("Test that params are passed to middleware") 1237 | { 1238 | router := New() 1239 | execLog = nil 1240 | router.Use(newParamsMiddleware("m1", "p")) 1241 | router.GET("/h2/:p", newHandler("h2")) 1242 | 1243 | req, _ := newRequest("GET", "/h2/paramvalue", nil) 1244 | router.ServeHTTP(w, req) 1245 | 1246 | assertExecLog([]string{"m1", "paramvalue", "h2"}) 1247 | } 1248 | 1249 | t.Log("Http Handler Middleware") 1250 | { 1251 | router := New() 1252 | w := httptest.NewRecorder() 1253 | execLog = nil 1254 | router.UseHandler(newHttpHandlerMiddleware("m5")) 1255 | router.Use(newParamsMiddleware("m6", "p")) 1256 | router.GET("/h7/:p", newHandler("h7")) 1257 | 1258 | req, _ := newRequest("GET", "/h7/paramvalue", nil) 1259 | router.ServeHTTP(w, req) 1260 | 1261 | req, _ = newRequest("GET", "/h7/anothervalue", nil) 1262 | router.ServeHTTP(w, req) 1263 | 1264 | assertExecLog([]string{"m5", "m6", "paramvalue", "h7", "m5", "m6", "anothervalue", "h7"}) 1265 | } 1266 | 1267 | } 1268 | 1269 | func BenchmarkRouterSimple(b *testing.B) { 1270 | router := New() 1271 | 1272 | router.GET("/", simpleHandler) 1273 | router.GET("/user/dimfeld", simpleHandler) 1274 | 1275 | r, _ := newRequest("GET", "/user/dimfeld", nil) 1276 | 1277 | benchRequest(b, router, r) 1278 | } 1279 | 1280 | func BenchmarkRouterRootWithPanicHandler(b *testing.B) { 1281 | router := New() 1282 | router.PanicHandler = SimplePanicHandler 1283 | 1284 | router.GET("/", simpleHandler) 1285 | router.GET("/user/dimfeld", simpleHandler) 1286 | 1287 | r, _ := newRequest("GET", "/", nil) 1288 | 1289 | benchRequest(b, router, r) 1290 | } 1291 | 1292 | func BenchmarkRouterRootWithoutPanicHandler(b *testing.B) { 1293 | router := New() 1294 | router.PanicHandler = nil 1295 | 1296 | router.GET("/", simpleHandler) 1297 | router.GET("/user/dimfeld", simpleHandler) 1298 | 1299 | r, _ := newRequest("GET", "/", nil) 1300 | 1301 | benchRequest(b, router, r) 1302 | } 1303 | 1304 | func BenchmarkRouterParam(b *testing.B) { 1305 | router := New() 1306 | 1307 | router.GET("/", simpleHandler) 1308 | router.GET("/user/:name", simpleHandler) 1309 | 1310 | r, _ := newRequest("GET", "/user/dimfeld", nil) 1311 | 1312 | benchRequest(b, router, r) 1313 | } 1314 | 1315 | func BenchmarkRouterLongParams(b *testing.B) { 1316 | router := New() 1317 | 1318 | router.GET("/", simpleHandler) 1319 | router.GET("/user/:name/:resource", simpleHandler) 1320 | 1321 | r, _ := newRequest("GET", "/user/aaaabbbbccccddddeeeeffff/asdfghjkl", nil) 1322 | 1323 | benchRequest(b, router, r) 1324 | } 1325 | --------------------------------------------------------------------------------