├── 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 |
182 |
183 |
184 |
185 | In {{ .FilePath }}:{{ .Line }}
186 |
187 |
188 |
189 |
190 |
191 | {{ range $lineNumber, $line := .Lines }}{{ $lineNumber }}{{ end }}
192 | |
193 |
194 | {{ range $lineNumber, $line := .Lines }}{{ $line }} {{ end }}
195 | |
196 |
197 |
198 |
Stack
199 |
{{ .Stack }}
200 |
Request
201 |
Method: {{ .Method }}
202 |
Parameters:
203 |
204 | {{ range $key, $value := .Params }}
205 | - {{ $key }}: {{ $value }}
206 | {{ end }}
207 |
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 [](https://travis-ci.org/dimfeld/httptreemux) [](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 |
--------------------------------------------------------------------------------