From bc452d92e321dc63d7b1190f9627ecd950ef1899 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 4 Nov 2017 13:51:27 -0700 Subject: [PATCH 01/89] [build] Allow tip failures (#312) [build] Allow tip failures --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index a319fdc4..3302233f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,8 @@ matrix: - go: 1.8 - go: 1.9 - go: tip + allow_failures: + - go: tip install: - # Skip From 9f48112f18a17f59ef17cfd7eea0d8c955504ebc Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sat, 4 Nov 2017 21:08:26 -0700 Subject: [PATCH 02/89] [docs] Document router.Match (#313) * Document router.Match The return values are getting confusing. Hopefully this helps. * Simplify some language. * Remove double the --- mux.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mux.go b/mux.go index cf4ac54b..a0d55e56 100644 --- a/mux.go +++ b/mux.go @@ -64,7 +64,17 @@ type Router struct { useEncodedPath bool } -// Match matches registered routes against the request. +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. func (r *Router) Match(req *http.Request, match *RouteMatch) bool { for _, route := range r.routes { if route.Match(req, match) { From 7f08801859139f86dfafd1c296e2cba9a80d292e Mon Sep 17 00:00:00 2001 From: Roberto Santalla Date: Sun, 5 Nov 2017 18:23:20 +0100 Subject: [PATCH 03/89] MatchErr is set to ErrNotFound if NotFoundHandler is used (#311) --- mux.go | 14 ++++-- mux_test.go | 129 ++++++++++++++++++++++++++++++++++++---------------- route.go | 6 ++- 3 files changed, 106 insertions(+), 43 deletions(-) diff --git a/mux.go b/mux.go index a0d55e56..49de7892 100644 --- a/mux.go +++ b/mux.go @@ -14,6 +14,7 @@ import ( var ( ErrMethodMismatch = errors.New("method is not allowed") + ErrNotFound = errors.New("no matching route was found") ) // NewRouter returns a new router instance. @@ -82,16 +83,23 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool { } } - if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil { - match.Handler = r.MethodNotAllowedHandler - return true + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } else { + return false + } } // Closest match for a router (includes sub-routers) if r.NotFoundHandler != nil { match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound return true } + + match.MatchErr = ErrNotFound return false } diff --git a/mux_test.go b/mux_test.go index 6c7f83d1..6c7e30d1 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1877,6 +1877,96 @@ func TestSubrouterHeader(t *testing.T) { } } +func TestNoMatchMethodErrorHandler(t *testing.T) { + func1 := func(w http.ResponseWriter, r *http.Request) {} + + r := NewRouter() + r.HandleFunc("/", func1).Methods("GET", "POST") + + req, _ := http.NewRequest("PUT", "http://localhost/", nil) + match := new(RouteMatch) + matched := r.Match(req, match) + + if matched { + t.Error("Should not have matched route for methods") + } + + if match.MatchErr != ErrMethodMismatch { + t.Error("Should get ErrMethodMismatch error") + } + + resp := NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != 405 { + t.Errorf("Expecting code %v", 405) + } + + // Add matching route + r.HandleFunc("/", func1).Methods("PUT") + + match = new(RouteMatch) + matched = r.Match(req, match) + + if !matched { + t.Error("Should have matched route for methods") + } + + if match.MatchErr != nil { + t.Error("Should not have any matching error. Found:", match.MatchErr) + } +} + +func TestErrMatchNotFound(t *testing.T) { + emptyHandler := func(w http.ResponseWriter, r *http.Request) {} + + r := NewRouter() + r.HandleFunc("/", emptyHandler) + s := r.PathPrefix("/sub/").Subrouter() + s.HandleFunc("/", emptyHandler) + + // Regular 404 not found + req, _ := http.NewRequest("GET", "/sub/whatever", nil) + match := new(RouteMatch) + matched := r.Match(req, match) + + if matched { + t.Errorf("Subrouter should not have matched that, got %v", match.Route) + } + // Even without a custom handler, MatchErr is set to ErrNotFound + if match.MatchErr != ErrNotFound { + t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) + } + + // Now lets add a 404 handler to subrouter + s.NotFoundHandler = http.NotFoundHandler() + req, _ = http.NewRequest("GET", "/sub/whatever", nil) + + // Test the subrouter first + match = new(RouteMatch) + matched = s.Match(req, match) + // Now we should get a match + if !matched { + t.Errorf("Subrouter should have matched %s", req.RequestURI) + } + // But MatchErr should be set to ErrNotFound anyway + if match.MatchErr != ErrNotFound { + t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) + } + + // Now test the parent (MatchErr should propagate) + match = new(RouteMatch) + matched = r.Match(req, match) + + // Now we should get a match + if !matched { + t.Errorf("Router should have matched %s via subrouter", req.RequestURI) + } + // But MatchErr should be set to ErrNotFound anyway + if match.MatchErr != ErrNotFound { + t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) + } +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int @@ -1943,42 +2033,3 @@ func newRequest(method, url string) *http.Request { } return req } - -func TestNoMatchMethodErrorHandler(t *testing.T) { - func1 := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/", func1).Methods("GET", "POST") - - req, _ := http.NewRequest("PUT", "http://localhost/", nil) - match := new(RouteMatch) - matched := r.Match(req, match) - - if matched { - t.Error("Should not have matched route for methods") - } - - if match.MatchErr != ErrMethodMismatch { - t.Error("Should get ErrMethodMismatch error") - } - - resp := NewRecorder() - r.ServeHTTP(resp, req) - if resp.Code != 405 { - t.Errorf("Expecting code %v", 405) - } - - // Add matching route - r.HandleFunc("/", func1).Methods("PUT") - - match = new(RouteMatch) - matched = r.Match(req, match) - - if !matched { - t.Error("Should have matched route for methods") - } - - if match.MatchErr != nil { - t.Error("Should not have any matching error. Found:", match.MatchErr) - } -} diff --git a/route.go b/route.go index 96f746c2..69aeae79 100644 --- a/route.go +++ b/route.go @@ -72,7 +72,11 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { return false } - match.MatchErr = nil + if match.MatchErr == ErrMethodMismatch { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + } + // Yay, we have a match. Let's collect some info about it. if match.Route == nil { match.Route = r From 2d5fef06b891c971b14aa6f71ca5ab6c03a36e0e Mon Sep 17 00:00:00 2001 From: Chris Dostert Date: Wed, 8 Nov 2017 19:54:02 -0800 Subject: [PATCH 04/89] [docs] fix outdated UseEncodedPath method docs (#314) https://github.com/gorilla/mux/pull/306 changed UseEncodedPath to use native go encoded path handling so cautions in it's docs are no longer applicable. --- mux.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mux.go b/mux.go index 49de7892..9aec0fac 100644 --- a/mux.go +++ b/mux.go @@ -196,10 +196,6 @@ func (r *Router) SkipClean(value bool) *Router { // UseEncodedPath tells the router to match the encoded original path // to the routes. // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". -// This behavior has the drawback of needing to match routes against -// r.RequestURI instead of r.URL.Path. Any modifications (such as http.StripPrefix) -// to r.URL.Path will not affect routing when this flag is on and thus may -// induce unintended behavior. // // If not called, the router will match the unencoded path to the routes. // For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" From 4a3d4f3dd2a7b32b26dc262a143440e75767f94d Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 28 Nov 2017 11:51:17 -0800 Subject: [PATCH 05/89] [bugfix] Fix method subrouter handler matching (#300) (#317) * Test method-based subrouters for multiple matching paths * Pass TestMethodsSubrouter * Change http.Method* constants to string literals - Make compatible with Go v1.5 * Make TestMethodsSubrouter stateless and concurrent * Remove t.Run and break up tests for concurrency * Use backticks to remove quote escaping * Remove global method handlers and HTTP method constants --- mux_test.go | 312 ++++++++++++++++++++++++++++++++++++++++++++++++++++ route.go | 2 + 2 files changed, 314 insertions(+) diff --git a/mux_test.go b/mux_test.go index 6c7e30d1..c39cb86a 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1967,6 +1967,318 @@ func TestErrMatchNotFound(t *testing.T) { } } +// methodsSubrouterTest models the data necessary for testing handler +// matching for subrouters created after HTTP methods matcher registration. +type methodsSubrouterTest struct { + title string + wantCode int + router *Router + // method is the input into the request and expected response + method string + // input request path + path string + // redirectTo is the expected location path for strict-slash matches + redirectTo string +} + +// methodHandler writes the method string in response. +func methodHandler(method string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(method)) + } +} + +// TestMethodsSubrouterCatchall matches handlers for subrouters where a +// catchall handler is set for a mis-matching method. +func TestMethodsSubrouterCatchall(t *testing.T) { + t.Parallel() + + router := NewRouter() + router.Methods("PATCH").Subrouter().PathPrefix("/").HandlerFunc(methodHandler("PUT")) + router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) + router.Methods("POST").Subrouter().HandleFunc("/foo", methodHandler("POST")) + router.Methods("DELETE").Subrouter().HandleFunc("/foo", methodHandler("DELETE")) + + tests := []methodsSubrouterTest{ + { + title: "match GET handler", + router: router, + path: "http://localhost/foo", + method: "GET", + wantCode: http.StatusOK, + }, + { + title: "match POST handler", + router: router, + method: "POST", + path: "http://localhost/foo", + wantCode: http.StatusOK, + }, + { + title: "match DELETE handler", + router: router, + method: "DELETE", + path: "http://localhost/foo", + wantCode: http.StatusOK, + }, + { + title: "disallow PUT method", + router: router, + method: "PUT", + path: "http://localhost/foo", + wantCode: http.StatusMethodNotAllowed, + }, + } + + for _, test := range tests { + testMethodsSubrouter(t, test) + } +} + +// TestMethodsSubrouterStrictSlash matches handlers on subrouters with +// strict-slash matchers. +func TestMethodsSubrouterStrictSlash(t *testing.T) { + t.Parallel() + + router := NewRouter() + sub := router.PathPrefix("/").Subrouter() + sub.StrictSlash(true).Path("/foo").Methods("GET").Subrouter().HandleFunc("", methodHandler("GET")) + sub.StrictSlash(true).Path("/foo/").Methods("PUT").Subrouter().HandleFunc("/", methodHandler("PUT")) + sub.StrictSlash(true).Path("/foo/").Methods("POST").Subrouter().HandleFunc("/", methodHandler("POST")) + + tests := []methodsSubrouterTest{ + { + title: "match POST handler", + router: router, + method: "POST", + path: "http://localhost/foo/", + wantCode: http.StatusOK, + }, + { + title: "match GET handler", + router: router, + method: "GET", + path: "http://localhost/foo", + wantCode: http.StatusOK, + }, + { + title: "match POST handler, redirect strict-slash", + router: router, + method: "POST", + path: "http://localhost/foo", + redirectTo: "http://localhost/foo/", + wantCode: http.StatusMovedPermanently, + }, + { + title: "match GET handler, redirect strict-slash", + router: router, + method: "GET", + path: "http://localhost/foo/", + redirectTo: "http://localhost/foo", + wantCode: http.StatusMovedPermanently, + }, + { + title: "disallow DELETE method", + router: router, + method: "DELETE", + path: "http://localhost/foo", + wantCode: http.StatusMethodNotAllowed, + }, + } + + for _, test := range tests { + testMethodsSubrouter(t, test) + } +} + +// TestMethodsSubrouterPathPrefix matches handlers on subrouters created +// on a router with a path prefix matcher and method matcher. +func TestMethodsSubrouterPathPrefix(t *testing.T) { + t.Parallel() + + router := NewRouter() + router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) + router.PathPrefix("/1").Methods("DELETE").Subrouter().HandleFunc("/2", methodHandler("DELETE")) + router.PathPrefix("/1").Methods("PUT").Subrouter().HandleFunc("/2", methodHandler("PUT")) + router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST2")) + + tests := []methodsSubrouterTest{ + { + title: "match first POST handler", + router: router, + method: "POST", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "match DELETE handler", + router: router, + method: "DELETE", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "match PUT handler", + router: router, + method: "PUT", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "disallow PATCH method", + router: router, + method: "PATCH", + path: "http://localhost/1/2", + wantCode: http.StatusMethodNotAllowed, + }, + } + + for _, test := range tests { + testMethodsSubrouter(t, test) + } +} + +// TestMethodsSubrouterSubrouter matches handlers on subrouters produced +// from method matchers registered on a root subrouter. +func TestMethodsSubrouterSubrouter(t *testing.T) { + t.Parallel() + + router := NewRouter() + sub := router.PathPrefix("/1").Subrouter() + sub.Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) + sub.Methods("GET").Subrouter().HandleFunc("/2", methodHandler("GET")) + sub.Methods("PATCH").Subrouter().HandleFunc("/2", methodHandler("PATCH")) + sub.HandleFunc("/2", methodHandler("PUT")).Subrouter().Methods("PUT") + sub.HandleFunc("/2", methodHandler("POST2")).Subrouter().Methods("POST") + + tests := []methodsSubrouterTest{ + { + title: "match first POST handler", + router: router, + method: "POST", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "match GET handler", + router: router, + method: "GET", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "match PATCH handler", + router: router, + method: "PATCH", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "match PUT handler", + router: router, + method: "PUT", + path: "http://localhost/1/2", + wantCode: http.StatusOK, + }, + { + title: "disallow DELETE method", + router: router, + method: "DELETE", + path: "http://localhost/1/2", + wantCode: http.StatusMethodNotAllowed, + }, + } + + for _, test := range tests { + testMethodsSubrouter(t, test) + } +} + +// TestMethodsSubrouterPathVariable matches handlers on matching paths +// with path variables in them. +func TestMethodsSubrouterPathVariable(t *testing.T) { + t.Parallel() + + router := NewRouter() + router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) + router.Methods("POST").Subrouter().HandleFunc("/{any}", methodHandler("POST")) + router.Methods("DELETE").Subrouter().HandleFunc("/1/{any}", methodHandler("DELETE")) + router.Methods("PUT").Subrouter().HandleFunc("/1/{any}", methodHandler("PUT")) + + tests := []methodsSubrouterTest{ + { + title: "match GET handler", + router: router, + method: "GET", + path: "http://localhost/foo", + wantCode: http.StatusOK, + }, + { + title: "match POST handler", + router: router, + method: "POST", + path: "http://localhost/foo", + wantCode: http.StatusOK, + }, + { + title: "match DELETE handler", + router: router, + method: "DELETE", + path: "http://localhost/1/foo", + wantCode: http.StatusOK, + }, + { + title: "match PUT handler", + router: router, + method: "PUT", + path: "http://localhost/1/foo", + wantCode: http.StatusOK, + }, + { + title: "disallow PATCH method", + router: router, + method: "PATCH", + path: "http://localhost/1/foo", + wantCode: http.StatusMethodNotAllowed, + }, + } + + for _, test := range tests { + testMethodsSubrouter(t, test) + } +} + +// testMethodsSubrouter runs an individual methodsSubrouterTest. +func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) { + // Execute request + req, _ := http.NewRequest(test.method, test.path, nil) + resp := NewRecorder() + test.router.ServeHTTP(resp, req) + + switch test.wantCode { + case http.StatusMethodNotAllowed: + if resp.Code != http.StatusMethodNotAllowed { + t.Errorf(`(%s) Expected "405 Method Not Allowed", but got %d code`, test.title, resp.Code) + } else if matchedMethod := resp.Body.String(); matchedMethod != "" { + t.Errorf(`(%s) Expected "405 Method Not Allowed", but %q handler was called`, test.title, matchedMethod) + } + + case http.StatusMovedPermanently: + if gotLocation := resp.HeaderMap.Get("Location"); gotLocation != test.redirectTo { + t.Errorf("(%s) Expected %q route-match to redirect to %q, but got %q", test.title, test.method, test.redirectTo, gotLocation) + } + + case http.StatusOK: + if matchedMethod := resp.Body.String(); matchedMethod != test.method { + t.Errorf("(%s) Expected %q handler to be called, but %q handler was called", test.title, test.method, matchedMethod) + } + + default: + expectedCodes := []int{http.StatusMethodNotAllowed, http.StatusMovedPermanently, http.StatusOK} + t.Errorf("(%s) Expected wantCode to be one of: %v, but got %d", test.title, expectedCodes, test.wantCode) + } +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int diff --git a/route.go b/route.go index 69aeae79..2b3de8bf 100644 --- a/route.go +++ b/route.go @@ -75,6 +75,8 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { if match.MatchErr == ErrMethodMismatch { // We found a route which matches request method, clear MatchErr match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler } // Yay, we have a match. Let's collect some info about it. From 65ec7248c53f499f6b480655e019e0b9d7a6ce11 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Tue, 28 Nov 2017 16:00:09 -0800 Subject: [PATCH 06/89] Create ISSUE_TEMPLATE.md (#318) --- ISSUE_TEMPLATE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..232be82e --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +**What version of Go are you running?** (Paste the output of `go version`) + + +**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`) + + +**Describe your problem** (and what you have tried so far) + + +**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it) + From c572efe4294d5a0e354e01f2ddaa8b1f0c3cb3dd Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 2 Dec 2017 12:38:52 -0800 Subject: [PATCH 07/89] [docs] Note StrictSlash re-direct behaviour #308 (#321) * [docs] Note StrictSlash re-direct behaviour #308 * StrictSlash enabled routes return a 301 to the client * As per the HTTP standards, non-idempotent methods, such as POST or PUT, will be followed with a GET by the client * Users should use middleware if they wish to change this behaviour to return a HTTP 308. * Update description of StrictSlash --- mux.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mux.go b/mux.go index 9aec0fac..5fd5fa83 100644 --- a/mux.go +++ b/mux.go @@ -164,13 +164,18 @@ func (r *Router) GetRoute(name string) *Route { // StrictSlash defines the trailing slash behavior for new routes. The initial // value is false. // -// When true, if the route path is "/path/", accessing "/path" will redirect +// When true, if the route path is "/path/", accessing "/path" will perform a redirect // to the former and vice versa. In other words, your application will always // see the path as specified in the route. // // When false, if the route path is "/path", accessing "/path/" will not match // this route and vice versa. // +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// // Special case: when a route sets a path prefix using the PathPrefix() method, // strict slash is ignored for that route because the redirect behavior can't // be determined from a prefix alone. However, any subrouters created from that From 7904d2e42e7ebbdb4a6eb3e57eb201b11df25c57 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 4 Dec 2017 08:11:14 -0800 Subject: [PATCH 08/89] [docs] Add example usage for Route.HeadersRegexp (#320) * Add example usage for Route.HeadersRegexp * Improve example_route_test.go style --- example_route_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++ route.go | 3 ++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 example_route_test.go diff --git a/example_route_test.go b/example_route_test.go new file mode 100644 index 00000000..11255707 --- /dev/null +++ b/example_route_test.go @@ -0,0 +1,51 @@ +package mux_test + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" +) + +// This example demonstrates setting a regular expression matcher for +// the header value. A plain word will match any value that contains a +// matching substring as if the pattern was wrapped with `.*`. +func ExampleRoute_HeadersRegexp() { + r := mux.NewRouter() + route := r.NewRoute().HeadersRegexp("Accept", "html") + + req1, _ := http.NewRequest("GET", "example.com", nil) + req1.Header.Add("Accept", "text/plain") + req1.Header.Add("Accept", "text/html") + + req2, _ := http.NewRequest("GET", "example.com", nil) + req2.Header.Set("Accept", "application/xhtml+xml") + + matchInfo := &mux.RouteMatch{} + fmt.Printf("Match: %v %q\n", route.Match(req1, matchInfo), req1.Header["Accept"]) + fmt.Printf("Match: %v %q\n", route.Match(req2, matchInfo), req2.Header["Accept"]) + // Output: + // Match: true ["text/plain" "text/html"] + // Match: true ["application/xhtml+xml"] +} + +// This example demonstrates setting a strict regular expression matcher +// for the header value. Using the start and end of string anchors, the +// value must be an exact match. +func ExampleRoute_HeadersRegexp_exactMatch() { + r := mux.NewRouter() + route := r.NewRoute().HeadersRegexp("Origin", "^https://example.co$") + + yes, _ := http.NewRequest("GET", "example.co", nil) + yes.Header.Set("Origin", "https://example.co") + + no, _ := http.NewRequest("GET", "example.co.uk", nil) + no.Header.Set("Origin", "https://example.co.uk") + + matchInfo := &mux.RouteMatch{} + fmt.Printf("Match: %v %q\n", route.Match(yes, matchInfo), yes.Header["Origin"]) + fmt.Printf("Match: %v %q\n", route.Match(no, matchInfo), no.Header["Origin"]) + // Output: + // Match: true ["https://example.co"] + // Match: false ["https://example.co.uk"] +} diff --git a/route.go b/route.go index 2b3de8bf..d0a71a92 100644 --- a/route.go +++ b/route.go @@ -258,7 +258,8 @@ func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { // "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both the request header matches both regular expressions. -// It the value is an empty string, it will match any value if the key is set. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. func (r *Route) HeadersRegexp(pairs ...string) *Route { if r.err == nil { var headers map[string]*regexp.Regexp From 5ab525f4fb1678e197ae59401e9050fa0b6cb5fd Mon Sep 17 00:00:00 2001 From: Zak Chitty Date: Sat, 9 Dec 2017 03:08:15 +1100 Subject: [PATCH 09/89] Public test API to set URL params (#322) * Add a function to set url params for test * [docs] add justification for SetURLVars and description of alternative approach to setting url vars. * rename SetURLParams to SetURLVars as this is more descriptive. * rename testing to testing_helpers as this is more descriptive. * [docs] add stipulation to SetURLVars that it should only be used for testing purposes --- test_helpers.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 test_helpers.go diff --git a/test_helpers.go b/test_helpers.go new file mode 100644 index 00000000..8b2c4a4c --- /dev/null +++ b/test_helpers.go @@ -0,0 +1,18 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return setVars(r, val) +} From 512169e5d707b96d6306216743bb7884a2b3a3c9 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 5 Jan 2018 10:40:59 -0800 Subject: [PATCH 10/89] refactor routeRegexp, particularily newRouteRegexp. (#328) The existing options matchPrefix, matchHost, and matchQueries are mutually exclusive so there's no point in having a separate boolean argument for each one. It's clearer if there's a single type variable. strictSlash and useEncodedPath were also being passed as naked bools so I've wrapped these in a struct called routeRegexpOptions for more clarity at the call site. --- mux_test.go | 2 +- old_test.go | 2 +- regexp.go | 74 +++++++++++++++++++++++++++++------------------------ route.go | 21 ++++++++------- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/mux_test.go b/mux_test.go index c39cb86a..9e93c983 100644 --- a/mux_test.go +++ b/mux_test.go @@ -25,7 +25,7 @@ func (r *Route) GoString() string { } func (r *routeRegexp) GoString() string { - return fmt.Sprintf("&routeRegexp{template: %q, matchHost: %t, matchQuery: %t, strictSlash: %t, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.matchHost, r.matchQuery, r.strictSlash, r.regexp.String(), r.reverse, r.varsN, r.varsR) + return fmt.Sprintf("&routeRegexp{template: %q, regexpType: %v, options: %v, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.regexpType, r.options, r.regexp.String(), r.reverse, r.varsN, r.varsR) } type routeTest struct { diff --git a/old_test.go b/old_test.go index 3751e472..b228983c 100644 --- a/old_test.go +++ b/old_test.go @@ -681,7 +681,7 @@ func TestNewRegexp(t *testing.T) { } for pattern, paths := range tests { - p, _ = newRouteRegexp(pattern, false, false, false, false, false) + p, _ = newRouteRegexp(pattern, regexpTypePath, routeRegexpOptions{}) for path, result := range paths { matches = p.regexp.FindStringSubmatch(path) if result == nil { diff --git a/regexp.go b/regexp.go index e83213b7..2b57e562 100644 --- a/regexp.go +++ b/regexp.go @@ -14,6 +14,20 @@ import ( "strings" ) +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = 0 + regexpTypeHost regexpType = 1 + regexpTypePrefix regexpType = 2 + regexpTypeQuery regexpType = 3 +) + // newRouteRegexp parses a route template and returns a routeRegexp, // used to match a host, a path or a query string. // @@ -24,7 +38,7 @@ import ( // Previously we accepted only Python-like identifiers for variable // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that // name and pattern can't be empty, and names can't contain a colon. -func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*routeRegexp, error) { +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { // Check if it is well-formed. idxs, errBraces := braceIndices(tpl) if errBraces != nil { @@ -34,19 +48,18 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, template := tpl // Now let's parse it. defaultPattern := "[^/]+" - if matchQuery { + if typ == regexpTypeQuery { defaultPattern = ".*" - } else if matchHost { + } else if typ == regexpTypeHost { defaultPattern = "[^.]+" - matchPrefix = false } // Only match strict slash if not matching - if matchPrefix || matchHost || matchQuery { - strictSlash = false + if typ != regexpTypePath { + options.strictSlash = false } // Set a flag for strictSlash. endSlash := false - if strictSlash && strings.HasSuffix(tpl, "/") { + if options.strictSlash && strings.HasSuffix(tpl, "/") { tpl = tpl[:len(tpl)-1] endSlash = true } @@ -88,16 +101,16 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, // Add the remaining. raw := tpl[end:] pattern.WriteString(regexp.QuoteMeta(raw)) - if strictSlash { + if options.strictSlash { pattern.WriteString("[/]?") } - if matchQuery { + if typ == regexpTypeQuery { // Add the default pattern if the query value is empty if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { pattern.WriteString(defaultPattern) } } - if !matchPrefix { + if typ != regexpTypePrefix { pattern.WriteByte('$') } reverse.WriteString(raw) @@ -118,15 +131,13 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, // Done! return &routeRegexp{ - template: template, - matchHost: matchHost, - matchQuery: matchQuery, - strictSlash: strictSlash, - useEncodedPath: useEncodedPath, - regexp: reg, - reverse: reverse.String(), - varsN: varsN, - varsR: varsR, + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, }, nil } @@ -135,15 +146,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, type routeRegexp struct { // The unmodified template. template string - // True for host match, false for path or query string match. - matchHost bool - // True for query string match, false for path and host match. - matchQuery bool - // The strictSlash value defined on the route, but disabled if PathPrefix was used. - strictSlash bool - // Determines whether to use encoded req.URL.EnscapedPath() or unencoded - // req.URL.Path for path matching - useEncodedPath bool + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions // Expanded regexp. regexp *regexp.Regexp // Reverse template. @@ -156,12 +162,12 @@ type routeRegexp struct { // Match matches the regexp against the URL host or path. func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { - if !r.matchHost { - if r.matchQuery { + if r.regexpType != regexpTypeHost { + if r.regexpType == regexpTypeQuery { return r.matchQueryString(req) } path := req.URL.Path - if r.useEncodedPath { + if r.options.useEncodedPath { path = req.URL.EscapedPath() } return r.regexp.MatchString(path) @@ -178,7 +184,7 @@ func (r *routeRegexp) url(values map[string]string) (string, error) { if !ok { return "", fmt.Errorf("mux: missing route variable %q", v) } - if r.matchQuery { + if r.regexpType == regexpTypeQuery { value = url.QueryEscape(value) } urlValues[k] = value @@ -203,7 +209,7 @@ func (r *routeRegexp) url(values map[string]string) (string, error) { // For a URL with foo=bar&baz=ding, we return only the relevant key // value pair for the routeRegexp. func (r *routeRegexp) getURLQuery(req *http.Request) string { - if !r.matchQuery { + if r.regexpType != regexpTypeQuery { return "" } templateKey := strings.SplitN(r.template, "=", 2)[0] @@ -280,7 +286,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) if len(matches) > 0 { extractVars(path, matches, v.path.varsN, m.Vars) // Check if we should redirect. - if v.path.strictSlash { + if v.path.options.strictSlash { p1 := strings.HasSuffix(path, "/") p2 := strings.HasSuffix(v.path.template, "/") if p1 != p2 { diff --git a/route.go b/route.go index d0a71a92..4ce098d4 100644 --- a/route.go +++ b/route.go @@ -171,12 +171,12 @@ func (r *Route) addMatcher(m matcher) *Route { } // addRegexpMatcher adds a host or path matcher and builder to a route. -func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { if r.err != nil { return r.err } r.regexp = r.getRegexpGroup() - if !matchHost && !matchQuery { + if typ == regexpTypePath || typ == regexpTypePrefix { if len(tpl) > 0 && tpl[0] != '/' { return fmt.Errorf("mux: path must start with a slash, got %q", tpl) } @@ -184,7 +184,10 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl } } - rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash, r.useEncodedPath) + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) if err != nil { return err } @@ -193,7 +196,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery return err } } - if matchHost { + if typ == regexpTypeHost { if r.regexp.path != nil { if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { return err @@ -206,7 +209,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery return err } } - if matchQuery { + if typ == regexpTypeQuery { r.regexp.queries = append(r.regexp.queries, rr) } else { r.regexp.path = rr @@ -289,7 +292,7 @@ func (r *Route) HeadersRegexp(pairs ...string) *Route { // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). func (r *Route) Host(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, true, false, false) + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) return r } @@ -349,7 +352,7 @@ func (r *Route) Methods(methods ...string) *Route { // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). func (r *Route) Path(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, false, false, false) + r.err = r.addRegexpMatcher(tpl, regexpTypePath) return r } @@ -365,7 +368,7 @@ func (r *Route) Path(tpl string) *Route { // Also note that the setting of Router.StrictSlash() has no effect on routes // with a PathPrefix matcher. func (r *Route) PathPrefix(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, false, true, false) + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) return r } @@ -396,7 +399,7 @@ func (r *Route) Queries(pairs ...string) *Route { return nil } for i := 0; i < length; i += 2 { - if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { return r } } From 5bbbb5b2b5729b132181cc7f4aa3b3c973e9a0ed Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sun, 7 Jan 2018 07:57:08 -0800 Subject: [PATCH 11/89] [docs] Add graceful shutdown example (#329) --- README.md | 128 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 67a79e00..20094574 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Static Files](#static-files) * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) * [Full Example](#full-example) --- @@ -45,11 +46,11 @@ Let's start registering a couple of URL paths and handlers: ```go func main() { - r := mux.NewRouter() - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/products", ProductsHandler) - r.HandleFunc("/articles", ArticlesHandler) - http.Handle("/", r) + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) } ``` @@ -68,9 +69,9 @@ The names are used to create a map of route variables which can be retrieved cal ```go func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "Category: %v\n", vars["category"]) + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) } ``` @@ -122,7 +123,7 @@ r.Queries("key", "value") ```go r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { - return r.ProtoMajor == 0 + return r.ProtoMajor == 0 }) ``` @@ -243,24 +244,24 @@ request that matches "/static/*". This makes it easy to serve static files with ```go func main() { - var dir string + var dir string - flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") - flag.Parse() - r := mux.NewRouter() + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() - // This will serve files under http://localhost:8000/static/ - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) - srv := &http.Server{ - Handler: r, - Addr: "127.0.0.1:8000", - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } - log.Fatal(srv.ListenAndServe()) + log.Fatal(srv.ListenAndServe()) } ``` @@ -383,6 +384,69 @@ r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error }) ``` +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(ctx, wait) + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + ## Full Example Here's a complete, runnable example of a small `mux` based server: @@ -391,22 +455,22 @@ Here's a complete, runnable example of a small `mux` based server: package main import ( - "net/http" - "log" - "github.com/gorilla/mux" + "net/http" + "log" + "github.com/gorilla/mux" ) func YourHandler(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Gorilla!\n")) + w.Write([]byte("Gorilla!\n")) } func main() { - r := mux.NewRouter() - // Routes consist of a path and a handler function. - r.HandleFunc("/", YourHandler) + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) - // Bind to a port and pass our router in - log.Fatal(http.ListenAndServe(":8000", r)) + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) } ``` From 53c1911da2b537f792e7cafcb446b05ffe33b996 Mon Sep 17 00:00:00 2001 From: Roberto Santalla Date: Tue, 16 Jan 2018 18:23:47 +0100 Subject: [PATCH 12/89] [feat] Add middleware support as discussed in #293 (#294) * mux.Router now has a `Use` method that allows you to add middleware to request processing. --- README.md | 81 +++++++++++ doc.go | 65 +++++++++ middleware.go | 28 ++++ middleware_test.go | 336 +++++++++++++++++++++++++++++++++++++++++++++ mux.go | 9 ++ 5 files changed, 519 insertions(+) create mode 100644 middleware.go create mode 100644 middleware_test.go diff --git a/README.md b/README.md index 20094574..f9b3103f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) * [Full Example](#full-example) --- @@ -447,6 +448,86 @@ func main() { } ``` +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.AddMiddlewareFunc()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.AddMiddleware(simpleMw) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", 403) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{} +amw.Populate() + +r.AddMiddlewareFunc(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares *should* write to `ResponseWriter` if they *are* going to terminate the request, and they *should not* write to `ResponseWriter` if they *are not* going to terminate it. + ## Full Example Here's a complete, runnable example of a small `mux` based server: diff --git a/doc.go b/doc.go index cce30b2f..013f0889 100644 --- a/doc.go +++ b/doc.go @@ -238,5 +238,70 @@ as well: url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42") + +Since **vX.Y.Z**, mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed if a +match is found (including subrouters). Middlewares are defined using the de facto standard type: + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.AddMiddleware(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", 403) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. + */ package mux diff --git a/middleware.go b/middleware.go new file mode 100644 index 00000000..8f898675 --- /dev/null +++ b/middleware.go @@ -0,0 +1,28 @@ +package mux + +import "net/http" + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// MiddlewareFunc also implements the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf MiddlewareFunc) { + r.middlewares = append(r.middlewares, mwf) +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} diff --git a/middleware_test.go b/middleware_test.go new file mode 100644 index 00000000..93947e8c --- /dev/null +++ b/middleware_test.go @@ -0,0 +1,336 @@ +package mux + +import ( + "bytes" + "net/http" + "testing" +) + +type testMiddleware struct { + timesCalled uint +} + +func (tm *testMiddleware) Middleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tm.timesCalled++ + h.ServeHTTP(w, r) + }) +} + +func dummyHandler(w http.ResponseWriter, r *http.Request) {} + +func TestMiddlewareAdd(t *testing.T) { + router := NewRouter() + router.HandleFunc("/", dummyHandler).Methods("GET") + + mw := &testMiddleware{} + + router.useInterface(mw) + if len(router.middlewares) != 1 || router.middlewares[0] != mw { + t.Fatal("Middleware was not added correctly") + } + + router.Use(mw.Middleware) + if len(router.middlewares) != 2 { + t.Fatal("MiddlewareFunc method was not added correctly") + } + + banalMw := func(handler http.Handler) http.Handler { + return handler + } + router.Use(banalMw) + if len(router.middlewares) != 3 { + t.Fatal("MiddlewareFunc method was not added correctly") + } +} + +func TestMiddleware(t *testing.T) { + router := NewRouter() + router.HandleFunc("/", dummyHandler).Methods("GET") + + mw := &testMiddleware{} + router.useInterface(mw) + + rw := NewRecorder() + req := newRequest("GET", "/") + + // Test regular middleware call + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + + // Middleware should not be called for 404 + req = newRequest("GET", "/not/found") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + + // Middleware should not be called if there is a method mismatch + req = newRequest("POST", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + + // Add the middleware again as function + router.Use(mw.Middleware) + req = newRequest("GET", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 3 { + t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled) + } + +} + +func TestMiddlewareSubrouter(t *testing.T) { + router := NewRouter() + router.HandleFunc("/", dummyHandler).Methods("GET") + + subrouter := router.PathPrefix("/sub").Subrouter() + subrouter.HandleFunc("/x", dummyHandler).Methods("GET") + + mw := &testMiddleware{} + subrouter.useInterface(mw) + + rw := NewRecorder() + req := newRequest("GET", "/") + + router.ServeHTTP(rw, req) + if mw.timesCalled != 0 { + t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) + } + + req = newRequest("GET", "/sub/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 0 { + t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) + } + + req = newRequest("GET", "/sub/x") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + + req = newRequest("GET", "/sub/not/found") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + + router.useInterface(mw) + + req = newRequest("GET", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 2 { + t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled) + } + + req = newRequest("GET", "/sub/x") + router.ServeHTTP(rw, req) + if mw.timesCalled != 4 { + t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled) + } +} + +func TestMiddlewareExecution(t *testing.T) { + mwStr := []byte("Middleware\n") + handlerStr := []byte("Logic\n") + + router := NewRouter() + router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }) + + rw := NewRecorder() + req := newRequest("GET", "/") + + // Test handler-only call + router.ServeHTTP(rw, req) + + if bytes.Compare(rw.Body.Bytes(), handlerStr) != 0 { + t.Fatal("Handler response is not what it should be") + } + + // Test middleware call + rw = NewRecorder() + + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) + }) + + router.ServeHTTP(rw, req) + if bytes.Compare(rw.Body.Bytes(), append(mwStr, handlerStr...)) != 0 { + t.Fatal("Middleware + handler response is not what it should be") + } +} + +func TestMiddlewareNotFound(t *testing.T) { + mwStr := []byte("Middleware\n") + handlerStr := []byte("Logic\n") + + router := NewRouter() + router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }) + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) + }) + + // Test not found call with default handler + rw := NewRecorder() + req := newRequest("GET", "/notfound") + + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a 404") + } + + // Test not found call with custom handler + rw = NewRecorder() + req = newRequest("GET", "/notfound") + + router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Custom 404 handler")) + }) + router.ServeHTTP(rw, req) + + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a custom 404") + } +} + +func TestMiddlewareMethodMismatch(t *testing.T) { + mwStr := []byte("Middleware\n") + handlerStr := []byte("Logic\n") + + router := NewRouter() + router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }).Methods("GET") + + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) + }) + + // Test method mismatch + rw := NewRecorder() + req := newRequest("POST", "/") + + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + + // Test not found call + rw = NewRecorder() + req = newRequest("POST", "/") + + router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Method not allowed")) + }) + router.ServeHTTP(rw, req) + + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } +} + +func TestMiddlewareNotFoundSubrouter(t *testing.T) { + mwStr := []byte("Middleware\n") + handlerStr := []byte("Logic\n") + + router := NewRouter() + router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }) + + subrouter := router.PathPrefix("/sub/").Subrouter() + subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }) + + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) + }) + + // Test not found call for default handler + rw := NewRecorder() + req := newRequest("GET", "/sub/notfound") + + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a 404") + } + + // Test not found call with custom handler + rw = NewRecorder() + req = newRequest("GET", "/sub/notfound") + + subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Custom 404 handler")) + }) + router.ServeHTTP(rw, req) + + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a custom 404") + } +} + +func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { + mwStr := []byte("Middleware\n") + handlerStr := []byte("Logic\n") + + router := NewRouter() + router.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }) + + subrouter := router.PathPrefix("/sub/").Subrouter() + subrouter.HandleFunc("/", func(w http.ResponseWriter, e *http.Request) { + w.Write(handlerStr) + }).Methods("GET") + + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) + }) + + // Test method mismatch without custom handler + rw := NewRecorder() + req := newRequest("POST", "/sub/") + + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + + // Test method mismatch with custom handler + rw = NewRecorder() + req = newRequest("POST", "/sub/") + + router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Method not allowed")) + }) + router.ServeHTTP(rw, req) + + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } +} diff --git a/mux.go b/mux.go index 5fd5fa83..efabd241 100644 --- a/mux.go +++ b/mux.go @@ -63,6 +63,8 @@ type Router struct { KeepContext bool // see Router.UseEncodedPath(). This defines a flag for all routes. useEncodedPath bool + // Slice of middlewares to be called after a match is found + middlewares []middleware } // Match attempts to match the given request against the router's registered routes. @@ -79,6 +81,12 @@ type Router struct { func (r *Router) Match(req *http.Request, match *RouteMatch) bool { for _, route := range r.routes { if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } return true } } @@ -147,6 +155,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { if !r.KeepContext { defer contextClear(req) } + handler.ServeHTTP(w, req) } From 0b74e3d0fe68bbe7c66e693df167074f7f527e86 Mon Sep 17 00:00:00 2001 From: Kush Mansingh Date: Tue, 16 Jan 2018 17:43:47 -0500 Subject: [PATCH 13/89] Make shutdown docs compilable (#330) --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f9b3103f..fc1555a5 100644 --- a/README.md +++ b/README.md @@ -410,7 +410,7 @@ func main() { r := mux.NewRouter() // Add your routes as needed - + srv := &http.Server{ Addr: "0.0.0.0:8080", // Good practice to set timeouts to avoid Slowloris attacks. @@ -426,7 +426,7 @@ func main() { log.Println(err) } }() - + c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. @@ -436,7 +436,8 @@ func main() { <-c // Create a deadline to wait for. - ctx, cancel := context.WithTimeout(ctx, wait) + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() // Doesn't block if no connections, but will otherwise wait // until the timeout deadline. srv.Shutdown(ctx) @@ -502,7 +503,7 @@ func (amw *authenticationMiddleware) Populate() { func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Session-Token") - + if user, found := amw.tokenUsers[token]; found { // We found the token in our map log.Printf("Authenticated user %s\n", user) From 85e6bfff1ae85ae9c2db46ea91ce093c0abca066 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Tue, 16 Jan 2018 17:18:53 -0800 Subject: [PATCH 14/89] Update doc.go: r.AddMiddleware(...) -> r.Use(...) --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 013f0889..7f12986e 100644 --- a/doc.go +++ b/doc.go @@ -261,7 +261,7 @@ Middlewares can be added to a router using `Router.Use()`: r := mux.NewRouter() r.HandleFunc("/", handler) - r.AddMiddleware(simpleMw) + r.Use(simpleMw) A more complex authentication middleware, which maps session token to users, could be written as: From 63c5c2f1f01d8151addc1b09edc9f39a8a217993 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Tue, 16 Jan 2018 23:16:06 -0800 Subject: [PATCH 15/89] [docs] Fix Middleware docs typos (#332) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc1555a5..6fe65961 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ Typically, the returned handler is a closure which does something with the http. A very basic middleware which logs the URI of the request being handled could be written as: ```go -func simpleMw(next http.Handler) http.Handler { +func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Do stuff here log.Println(r.RequestURI) @@ -475,12 +475,12 @@ func simpleMw(next http.Handler) http.Handler { } ``` -Middlewares can be added to a router using `Router.AddMiddlewareFunc()`: +Middlewares can be added to a router using `Router.Use()`: ```go r := mux.NewRouter() r.HandleFunc("/", handler) -r.AddMiddleware(simpleMw) +r.Use(loggingMiddleware) ``` A more complex authentication middleware, which maps session token to users, could be written as: @@ -524,7 +524,7 @@ r.HandleFunc("/", handler) amw := authenticationMiddleware{} amw.Populate() -r.AddMiddlewareFunc(amw.Middleware) +r.Use(amw.Middleware) ``` Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares *should* write to `ResponseWriter` if they *are* going to terminate the request, and they *should not* write to `ResponseWriter` if they *are not* going to terminate it. From 69dae3b874ba34bcaa563d5e5b1680334bfd9b73 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Tue, 16 Jan 2018 23:16:36 -0800 Subject: [PATCH 16/89] [docs] Add testing example (#331) --- README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6fe65961..dc9e254e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -gorilla/mux -=== +# gorilla/mux + [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) @@ -29,6 +29,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) * [Middleware](#middleware) +* [Testing Handlers](#testing-handlers) * [Full Example](#full-example) --- @@ -178,6 +179,7 @@ s.HandleFunc("/{key}/", ProductHandler) // "/products/{key}/details" s.HandleFunc("/{key}/details", ProductDetailsHandler) ``` + ### Listing Routes Routes on a mux can be listed using the Router.Walk method—useful for generating documentation: @@ -241,7 +243,7 @@ func main() { Note that the path provided to `PathPrefix()` represents a "wildcard": calling `PathPrefix("/static/").Handler(...)` means that the handler will be passed any -request that matches "/static/*". This makes it easy to serve static files with mux: +request that matches "/static/\*". This makes it easy to serve static files with mux: ```go func main() { @@ -527,7 +529,130 @@ amw.Populate() r.Use(amw.Middleware) ``` -Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares *should* write to `ResponseWriter` if they *are* going to terminate the request, and they *should not* write to `ResponseWriter` if they *are not* going to terminate it. +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, t := tt { + path := fmt.Sprintf("/metrics/%s", t.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(MetricsHandler) + handler.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !t.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + t.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` ## Full Example From ad8790881febb89da2b487073a57e079fa4aaa6f Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Thu, 18 Jan 2018 09:53:57 -0800 Subject: [PATCH 17/89] [docs] Fix doc.go (#333) Addresses https://github.com/gorilla/mux/pull/294#discussion_r162309666 --- doc.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc.go b/doc.go index 7f12986e..b6dd1ffb 100644 --- a/doc.go +++ b/doc.go @@ -239,8 +239,7 @@ as well: "category", "technology", "id", "42") -Since **vX.Y.Z**, mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed if a -match is found (including subrouters). Middlewares are defined using the de facto standard type: +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. type MiddlewareFunc func(http.Handler) http.Handler From 3dbb9ed96e4b25ec143f9d484ad5695179f10d47 Mon Sep 17 00:00:00 2001 From: safeoy Date: Sat, 20 Jan 2018 12:20:16 +0800 Subject: [PATCH 18/89] README.md: add miss "time" (#336) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc9e254e..5028112a 100644 --- a/README.md +++ b/README.md @@ -401,6 +401,7 @@ import ( "net/http" "os" "os/signal" + "time" "github.com/gorilla/mux" ) From dc835075986d1932ee5495f32e3631770715fd6c Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 19 Jan 2018 20:47:48 -0800 Subject: [PATCH 19/89] [docs] README.md: Improve "walking routes" example. (#337) (#323) Fixes #323. Also removed the duplicate "listing routes" example. --- README.md | 145 ++++++++++++++++++++---------------------------------- 1 file changed, 52 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 5028112a..e56cdb3e 100644 --- a/README.md +++ b/README.md @@ -180,64 +180,6 @@ s.HandleFunc("/{key}/", ProductHandler) s.HandleFunc("/{key}/details", ProductDetailsHandler) ``` -### Listing Routes - -Routes on a mux can be listed using the Router.Walk method—useful for generating documentation: - -```go -package main - -import ( - "fmt" - "net/http" - "strings" - - "github.com/gorilla/mux" -) - -func handler(w http.ResponseWriter, r *http.Request) { - return -} - -func main() { - r := mux.NewRouter() - r.HandleFunc("/", handler) - r.HandleFunc("/products", handler).Methods("POST") - r.HandleFunc("/articles", handler).Methods("GET") - r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") - r.HandleFunc("/authors", handler).Queries("surname", "{surname}") - r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { - t, err := route.GetPathTemplate() - if err != nil { - return err - } - qt, err := route.GetQueriesTemplates() - if err != nil { - return err - } - // p will contain regular expression is compatible with regular expression in Perl, Python, and other languages. - // for instance the regular expression for path '/articles/{id}' will be '^/articles/(?P[^/]+)$' - p, err := route.GetPathRegexp() - if err != nil { - return err - } - // qr will contain a list of regular expressions with the same semantics as GetPathRegexp, - // just applied to the Queries pairs instead, e.g., 'Queries("surname", "{surname}") will return - // {"^surname=(?P.*)$}. Where each combined query pair will have an entry in the list. - qr, err := route.GetQueriesRegexp() - if err != nil { - return err - } - m, err := route.GetMethods() - if err != nil { - return err - } - fmt.Println(strings.Join(m, ","), strings.Join(qt, ","), strings.Join(qr, ","), t, p) - return nil - }) - http.Handle("/", r) -} -``` ### Static Files @@ -350,41 +292,58 @@ The `Walk` function on `mux.Router` can be used to visit all of the routes that the following prints all of the registered routes: ```go -r := mux.NewRouter() -r.HandleFunc("/", handler) -r.HandleFunc("/products", handler).Methods("POST") -r.HandleFunc("/articles", handler).Methods("GET") -r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") -r.HandleFunc("/authors", handler).Queries("surname", "{surname}") -r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { - t, err := route.GetPathTemplate() - if err != nil { - return err - } - qt, err := route.GetQueriesTemplates() - if err != nil { - return err - } - // p will contain a regular expression that is compatible with regular expressions in Perl, Python, and other languages. - // For example, the regular expression for path '/articles/{id}' will be '^/articles/(?P[^/]+)$'. - p, err := route.GetPathRegexp() - if err != nil { - return err - } - // qr will contain a list of regular expressions with the same semantics as GetPathRegexp, - // just applied to the Queries pairs instead, e.g., 'Queries("surname", "{surname}") will return - // {"^surname=(?P.*)$}. Where each combined query pair will have an entry in the list. - qr, err := route.GetQueriesRegexp() - if err != nil { - return err - } - m, err := route.GetMethods() - if err != nil { - return err - } - fmt.Println(strings.Join(m, ","), strings.Join(qt, ","), strings.Join(qr, ","), t, p) - return nil -}) +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} ``` ### Graceful Shutdown From 077b44c2cf82958550c79f5885ee64900cdcee22 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 19 Jan 2018 20:51:41 -0800 Subject: [PATCH 20/89] [docs] Document route.Get* methods consistently (#338) They actually return an error instead of an empty list. `GetMethods` happened to not return an error, but it should for consistency, so I added that as well. --- route.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/route.go b/route.go index 4ce098d4..9f261438 100644 --- a/route.go +++ b/route.go @@ -622,7 +622,7 @@ func (r *Route) GetPathRegexp() (string, error) { // route queries. // This is useful for building simple REST API documentation and for instrumentation // against third-party services. -// An empty list will be returned if the route does not have queries. +// An error will be returned if the route does not have queries. func (r *Route) GetQueriesRegexp() ([]string, error) { if r.err != nil { return nil, r.err @@ -641,7 +641,7 @@ func (r *Route) GetQueriesRegexp() ([]string, error) { // query matching. // This is useful for building simple REST API documentation and for instrumentation // against third-party services. -// An empty list will be returned if the route does not define queries. +// An error will be returned if the route does not define queries. func (r *Route) GetQueriesTemplates() ([]string, error) { if r.err != nil { return nil, r.err @@ -659,7 +659,7 @@ func (r *Route) GetQueriesTemplates() ([]string, error) { // GetMethods returns the methods the route matches against // This is useful for building simple REST API documentation and for instrumentation // against third-party services. -// An empty list will be returned if route does not have methods. +// An error will be returned if route does not have methods. func (r *Route) GetMethods() ([]string, error) { if r.err != nil { return nil, r.err @@ -669,7 +669,7 @@ func (r *Route) GetMethods() ([]string, error) { return []string(methods), nil } } - return nil, nil + return nil, errors.New("mux: route doesn't have methods") } // GetHostTemplate returns the template used to build the From 0fdf828bb28a79b519024e2e29915e851b73e580 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Fri, 19 Jan 2018 22:28:49 -0800 Subject: [PATCH 21/89] [docs] Clarify SetURLVars (#335) * [docs] Clarify SetURLVars Clarify in documentation that SetURLVars does not modify the given *htttp.Request, provide an example of usage. * Short and sweet function doc, example test. --- mux_test.go | 9 +++++++++ test_helpers.go | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 9e93c983..4591344e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -2248,6 +2248,15 @@ func TestMethodsSubrouterPathVariable(t *testing.T) { } } +func ExampleSetURLVars() { + req, _ := http.NewRequest("GET", "/foo", nil) + req = SetURLVars(req, map[string]string{"foo": "bar"}) + + fmt.Println(Vars(req)["foo"]) + + // Output: bar +} + // testMethodsSubrouter runs an individual methodsSubrouterTest. func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) { // Execute request diff --git a/test_helpers.go b/test_helpers.go index 8b2c4a4c..32ecffde 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -7,7 +7,8 @@ package mux import "net/http" // SetURLVars sets the URL variables for the given request, to be accessed via -// mux.Vars for testing route behaviour. +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. // // This API should only be used for testing purposes; it provides a way to // inject variables into the request context. Alternatively, URL variables From c0091a029979286890368b4c7b301261e448e242 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 19 Jan 2018 23:58:19 -0800 Subject: [PATCH 22/89] Create authentication middleware example. (#340) * Create authentication middleware example. For #339 * Fix example test filename. --- example_authentication_middleware_test.go | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 example_authentication_middleware_test.go diff --git a/example_authentication_middleware_test.go b/example_authentication_middleware_test.go new file mode 100644 index 00000000..bc468d44 --- /dev/null +++ b/example_authentication_middleware_test.go @@ -0,0 +1,46 @@ +package mux_test + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" +) + +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", 403) + } + }) +} + +func Example_authenticationMiddleware() { + r := mux.NewRouter() + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Do something here + }) + amw := authenticationMiddleware{} + amw.Populate() + r.Use(amw.Middleware) +} From d284fd84214eaf533d13ee880d2ab978ef81d7a2 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Mon, 26 Feb 2018 01:08:54 +0900 Subject: [PATCH 23/89] Modify 403 status code to const variable (#349) * Modify http status code to variable * Modify doc --- doc.go | 2 +- example_authentication_middleware_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index b6dd1ffb..38957dee 100644 --- a/doc.go +++ b/doc.go @@ -287,7 +287,7 @@ A more complex authentication middleware, which maps session token to users, cou log.Printf("Authenticated user %s\n", user) next.ServeHTTP(w, r) } else { - http.Error(w, "Forbidden", 403) + http.Error(w, "Forbidden", http.StatusForbidden) } }) } diff --git a/example_authentication_middleware_test.go b/example_authentication_middleware_test.go index bc468d44..67df3218 100644 --- a/example_authentication_middleware_test.go +++ b/example_authentication_middleware_test.go @@ -30,7 +30,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler log.Printf("Authenticated user %s\n", user) next.ServeHTTP(w, r) } else { - http.Error(w, "Forbidden", 403) + http.Error(w, "Forbidden", http.StatusForbidden) } }) } From 07ba1fd60e210dc5cc6f86ec25dc9ecc7c95c295 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Mon, 26 Feb 2018 14:11:51 +0900 Subject: [PATCH 24/89] Modify http status code to variable in README (#350) * Modify http status code to variable * Modify doc * Modify README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e56cdb3e..ad242494 100644 --- a/README.md +++ b/README.md @@ -473,7 +473,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler next.ServeHTTP(w, r) } else { // Write an error and stop the handler chain - http.Error(w, "Forbidden", 403) + http.Error(w, "Forbidden", http.StatusForbidden) } }) } From 4dbd923b0c9e99ff63ad54b0e9705ff92d3cdb06 Mon Sep 17 00:00:00 2001 From: Johan Svensson Date: Wed, 14 Mar 2018 17:31:26 +0100 Subject: [PATCH 25/89] Make Use() variadic (#355) Enables neater syntax when chaining several middleware functions. --- middleware.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/middleware.go b/middleware.go index 8f898675..ec79e5d7 100644 --- a/middleware.go +++ b/middleware.go @@ -18,8 +18,10 @@ func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { } // Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. -func (r *Router) Use(mwf MiddlewareFunc) { - r.middlewares = append(r.middlewares, mwf) +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } } // useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. From 94231ffd98496cbcb1c15b7bf2a9edfd5f852cd4 Mon Sep 17 00:00:00 2001 From: brandon-height <33162344+brandon-height@users.noreply.github.com> Date: Tue, 3 Apr 2018 11:23:30 -0700 Subject: [PATCH 26/89] Fix table-driven example documentation (#363) Prior to this change, the example documentation found in the README.md has an errant code which won't work in the table-driven code example. This change modifies the variable name from `t` to `tc` so it does not conflict with the `t *testing.T` struct definition. * Adds a range clause to the `for` statement * Modifies `for` statement scope to use `tc.shouldPass`, and `tc.routeVariable` Doc: https://github.com/gorilla/mux#testing-handlers --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ad242494..8f55ddcd 100644 --- a/README.md +++ b/README.md @@ -593,8 +593,8 @@ func TestMetricsHandler(t *testing.T) { {"adhadaeqm3k", false}, } - for _, t := tt { - path := fmt.Sprintf("/metrics/%s", t.routeVariable) + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) req, err := http.NewRequest("GET", path, nil) if err != nil { t.Fatal(err) @@ -606,9 +606,9 @@ func TestMetricsHandler(t *testing.T) { // In this case, our MetricsHandler returns a non-200 response // for a route variable it doesn't know about. - if rr.Code == http.StatusOK && !t.shouldPass { + if rr.Code == http.StatusOK && !tc.shouldPass { t.Errorf("handler should have failed on routeVariable %s: got %v want %v", - t.routeVariable, rr.Code, http.StatusOK) + tc.routeVariable, rr.Code, http.StatusOK) } } } From b57cb1605fd11ba2ecfa7f68992b4b9cc791934d Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Mon, 16 Apr 2018 13:45:19 -0700 Subject: [PATCH 27/89] [build] Update Go versions; add 1.10.x (#364) --- .travis.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3302233f..ad0935db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,12 @@ sudo: false matrix: include: - - go: 1.5 - - go: 1.6 - - go: 1.7 - - go: 1.8 - - go: 1.9 + - go: 1.5.x + - go: 1.6.x + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x - go: tip allow_failures: - go: tip From ded0c29b24f96f46cf349e6701b099db601cf8ec Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Mon, 30 Apr 2018 20:11:36 -0700 Subject: [PATCH 28/89] Fix linter issues (docs) (#370) --- middleware.go | 2 +- mux.go | 9 ++++++--- route.go | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/middleware.go b/middleware.go index ec79e5d7..cf6cfc33 100644 --- a/middleware.go +++ b/middleware.go @@ -12,7 +12,7 @@ type middleware interface { Middleware(handler http.Handler) http.Handler } -// MiddlewareFunc also implements the middleware interface. +// Middleware also implements the middleware interface. func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { return mw(handler) } diff --git a/mux.go b/mux.go index efabd241..5b39694e 100644 --- a/mux.go +++ b/mux.go @@ -13,8 +13,11 @@ import ( ) var ( + // ErrMethodMismatch is returned when the error in the request does not match + // the method defined against the route. ErrMethodMismatch = errors.New("method is not allowed") - ErrNotFound = errors.New("no matching route was found") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") ) // NewRouter returns a new router instance. @@ -95,9 +98,9 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool { if r.MethodNotAllowedHandler != nil { match.Handler = r.MethodNotAllowedHandler return true - } else { - return false } + + return false } // Closest match for a router (includes sub-routers) diff --git a/route.go b/route.go index 9f261438..cc37ad6c 100644 --- a/route.go +++ b/route.go @@ -43,6 +43,8 @@ type Route struct { buildVarsFunc BuildVarsFunc } +// SkipClean bypasses cleaning the path, which includes removing duplicate +// slashes and URL encoding. func (r *Route) SkipClean() bool { return r.skipClean } From 5e55a4adb89fb64e8b13bbd302eeedac7a4ba5d8 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Fri, 11 May 2018 18:30:14 -0700 Subject: [PATCH 29/89] Add CORSMethodMiddleware (#366) CORSMethodMiddleware sets the Access-Control-Allow-Methods response header on a request, by matching routes based only on paths. It also handles OPTIONS requests, by settings Access-Control-Allow-Methods, and then returning without calling the next HTTP handler. --- middleware.go | 44 +++++++++++++++++++++++++++++++++++++++++++- middleware_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ mux_test.go | 8 ++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/middleware.go b/middleware.go index cf6cfc33..fab9ae35 100644 --- a/middleware.go +++ b/middleware.go @@ -1,6 +1,9 @@ package mux -import "net/http" +import ( + "net/http" + "strings" +) // MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. // Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed @@ -28,3 +31,42 @@ func (r *Router) Use(mwf ...MiddlewareFunc) { func (r *Router) useInterface(mw middleware) { r.middlewares = append(r.middlewares, mw) } + +// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header +// on a request, by matching routes based only on paths. It also handles +// OPTIONS requests, by settings Access-Control-Allow-Methods, and then +// returning without calling the next http handler. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var allMethods []string + + err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { + for _, m := range route.matchers { + if _, ok := m.(*routeRegexp); ok { + if m.Match(req, &RouteMatch{}) { + methods, err := route.GetMethods() + if err != nil { + return err + } + + allMethods = append(allMethods, methods...) + } + break + } + } + return nil + }) + + if err == nil { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ",")) + + if req.Method == "OPTIONS" { + return + } + } + + next.ServeHTTP(w, req) + }) + } +} diff --git a/middleware_test.go b/middleware_test.go index 93947e8c..acf4e160 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -3,6 +3,7 @@ package mux import ( "bytes" "net/http" + "net/http/httptest" "testing" ) @@ -334,3 +335,43 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { t.Fatal("Middleware was called for a method mismatch") } } + +func TestCORSMethodMiddleware(t *testing.T) { + router := NewRouter() + + cases := []struct { + path string + response string + method string + testURL string + expectedAllowedMethods string + }{ + {"/g/{o}", "a", "POST", "/g/asdf", "POST,PUT,GET,OPTIONS"}, + {"/g/{o}", "b", "PUT", "/g/bla", "POST,PUT,GET,OPTIONS"}, + {"/g/{o}", "c", "GET", "/g/orilla", "POST,PUT,GET,OPTIONS"}, + {"/g", "d", "POST", "/g", "POST,OPTIONS"}, + } + + for _, tt := range cases { + router.HandleFunc(tt.path, stringHandler(tt.response)).Methods(tt.method) + } + + router.Use(CORSMethodMiddleware(router)) + + for _, tt := range cases { + rr := httptest.NewRecorder() + req := newRequest(tt.method, tt.testURL) + + router.ServeHTTP(rr, req) + + if rr.Body.String() != tt.response { + t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String()) + } + + allowedMethods := rr.HeaderMap.Get("Access-Control-Allow-Methods") + + if allowedMethods != tt.expectedAllowedMethods { + t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods) + } + } +} diff --git a/mux_test.go b/mux_test.go index 4591344e..af21329f 100644 --- a/mux_test.go +++ b/mux_test.go @@ -2315,6 +2315,14 @@ func stringMapEqual(m1, m2 map[string]string) bool { return true } +// stringHandler returns a handler func that writes a message 's' to the +// http.ResponseWriter. +func stringHandler(s string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(s)) + } +} + // newRequest is a helper function to create a new request with a method and url. // The request returned is a 'server' request as opposed to a 'client' one through // simulated write onto the wire and read off of the wire. From fdeb7bc314fecece4e261b048f5fee1cb80208fe Mon Sep 17 00:00:00 2001 From: Sean Walberg Date: Sat, 12 May 2018 23:09:30 -0400 Subject: [PATCH 30/89] [docs] Doc fix for testing variables in path (#374) The example in the README does not pass the request through a mux therefore the request variables from the path are never populated. Update the sample to create a minimum viable router to use. Fixes #373 --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f55ddcd..e424397a 100644 --- a/README.md +++ b/README.md @@ -601,8 +601,11 @@ func TestMetricsHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(MetricsHandler) - handler.ServeHTTP(rr, req) + + // Need to create a router that we can pass the request through so that the vars will be added to the context + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) // In this case, our MetricsHandler returns a non-200 response // for a route variable it doesn't know about. From e3702bed27f0d39777b0b37b664b6280e8ef8fbf Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 12 May 2018 20:22:33 -0700 Subject: [PATCH 31/89] [docs] Improve docstrings for middleware, skipclean (#375) --- middleware.go | 2 +- mux.go | 2 +- route.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/middleware.go b/middleware.go index fab9ae35..ceb812ce 100644 --- a/middleware.go +++ b/middleware.go @@ -15,7 +15,7 @@ type middleware interface { Middleware(handler http.Handler) http.Handler } -// Middleware also implements the middleware interface. +// Middleware allows MiddlewareFunc to implement the middleware interface. func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { return mw(handler) } diff --git a/mux.go b/mux.go index 5b39694e..4bbafa51 100644 --- a/mux.go +++ b/mux.go @@ -13,7 +13,7 @@ import ( ) var ( - // ErrMethodMismatch is returned when the error in the request does not match + // ErrMethodMismatch is returned when the method in the request does not match // the method defined against the route. ErrMethodMismatch = errors.New("method is not allowed") // ErrNotFound is returned when no route match is found. diff --git a/route.go b/route.go index cc37ad6c..a591d735 100644 --- a/route.go +++ b/route.go @@ -43,8 +43,8 @@ type Route struct { buildVarsFunc BuildVarsFunc } -// SkipClean bypasses cleaning the path, which includes removing duplicate -// slashes and URL encoding. +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. func (r *Route) SkipClean() bool { return r.skipClean } From c85619274f5d55a67491010f883a91f568b3a9b0 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Thu, 17 May 2018 10:36:23 -0700 Subject: [PATCH 32/89] [deps] Add go.mod for versioned Go (#376) --- go.mod | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..cfc8ede5 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/gorilla/mux From e0b5abaaae35242fa74372f26c24107711702a4e Mon Sep 17 00:00:00 2001 From: Jim Kalafut Date: Sat, 26 May 2018 15:17:21 -0700 Subject: [PATCH 33/89] Initialize user map (#371) --- example_authentication_middleware_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_authentication_middleware_test.go b/example_authentication_middleware_test.go index 67df3218..6f2ea86c 100644 --- a/example_authentication_middleware_test.go +++ b/example_authentication_middleware_test.go @@ -40,7 +40,7 @@ func Example_authenticationMiddleware() { r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Do something here }) - amw := authenticationMiddleware{} + amw := authenticationMiddleware{make(map[string]string)} amw.Populate() r.Use(amw.Middleware) } From cb4698366aa625048f3b815af6a0dea8aef9280a Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Tue, 5 Jun 2018 14:15:56 -0700 Subject: [PATCH 34/89] Update LICENSE & AUTHORS files. (#386) --- AUTHORS | 8 ++++++++ LICENSE | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..b722392e --- /dev/null +++ b/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of gorilla/mux authors for copyright purposes. +# +# Please keep the list sorted. + +Google LLC (https://opensource.google.com/) +Kamil Kisielk +Matt Silverlock +Rodrigo Moraes (https://github.com/moraes) diff --git a/LICENSE b/LICENSE index 0e5fb872..6903df63 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are From 815b8c6a2641cd336000b3fb159b5741d0dc0113 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Tue, 7 Aug 2018 00:50:18 -0700 Subject: [PATCH 35/89] Clarify behaviour of Name method if called multiple times. --- route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route.go b/route.go index a591d735..c8bb5c7e 100644 --- a/route.go +++ b/route.go @@ -137,7 +137,7 @@ func (r *Route) GetHandler() http.Handler { // Name ----------------------------------------------------------------------- // Name sets the name for the route, used to build URLs. -// If the name was registered already it will be overwritten. +// It is an error to call Name more than once on a route. func (r *Route) Name(name string) *Route { if r.name != "" { r.err = fmt.Errorf("mux: route already has name %q, can't set %q", From e48e440e4c92e3251d812f8ce7858944dfa3331c Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Tue, 7 Aug 2018 00:52:56 -0700 Subject: [PATCH 36/89] Add test for multiple calls to Name(). Fixes #394 --- mux_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mux_test.go b/mux_test.go index af21329f..31c7d51d 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1323,6 +1323,15 @@ func TestNamedRoutes(t *testing.T) { } } +func TestNameMultipleCalls(t *testing.T) { + r1 := NewRouter() + rt := r1.NewRoute().Name("foo").Name("bar") + err := rt.GetError() + if err == nil { + t.Errorf("Expected an error") + } +} + func TestStrictSlash(t *testing.T) { r := NewRouter() r.StrictSlash(true) From 962c5bed0721b50f2ba42684e9600dfde174770f Mon Sep 17 00:00:00 2001 From: Shalom Yerushalmy Date: Thu, 30 Aug 2018 17:23:24 +0300 Subject: [PATCH 37/89] Add 1.11 to build in travis (#398) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ad0935db..5da7addc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ matrix: - go: 1.8.x - go: 1.9.x - go: 1.10.x + - go: 1.11.x - go: tip allow_failures: - go: tip From 8771f97498c6be5fe95d2eb5fb882c18bbf7f361 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Sun, 2 Sep 2018 15:22:40 -0700 Subject: [PATCH 38/89] Drop support for Go < 1.7: remove gorilla/context (#391) * Drop support for Go < 1.7: remove gorilla/context * Remove Go < 1.7 from Travis CI config * Remove unneeded _native from context files --- .travis.yml | 2 -- context_native.go => context.go | 2 -- context_gorilla.go | 26 --------------- context_gorilla_test.go | 40 ----------------------- context_native_test.go => context_test.go | 2 -- 5 files changed, 72 deletions(-) rename context_native.go => context.go (95%) delete mode 100644 context_gorilla.go delete mode 100644 context_gorilla_test.go rename context_native_test.go => context_test.go (97%) diff --git a/.travis.yml b/.travis.yml index 5da7addc..1922f6bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ sudo: false matrix: include: - - go: 1.5.x - - go: 1.6.x - go: 1.7.x - go: 1.8.x - go: 1.9.x diff --git a/context_native.go b/context.go similarity index 95% rename from context_native.go rename to context.go index 209cbea7..13a4601a 100644 --- a/context_native.go +++ b/context.go @@ -1,5 +1,3 @@ -// +build go1.7 - package mux import ( diff --git a/context_gorilla.go b/context_gorilla.go deleted file mode 100644 index d7adaa8f..00000000 --- a/context_gorilla.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build !go1.7 - -package mux - -import ( - "net/http" - - "github.com/gorilla/context" -) - -func contextGet(r *http.Request, key interface{}) interface{} { - return context.Get(r, key) -} - -func contextSet(r *http.Request, key, val interface{}) *http.Request { - if val == nil { - return r - } - - context.Set(r, key, val) - return r -} - -func contextClear(r *http.Request) { - context.Clear(r) -} diff --git a/context_gorilla_test.go b/context_gorilla_test.go deleted file mode 100644 index ffaf384c..00000000 --- a/context_gorilla_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// +build !go1.7 - -package mux - -import ( - "net/http" - "testing" - - "github.com/gorilla/context" -) - -// Tests that the context is cleared or not cleared properly depending on -// the configuration of the router -func TestKeepContext(t *testing.T) { - func1 := func(w http.ResponseWriter, r *http.Request) {} - - r := NewRouter() - r.HandleFunc("/", func1).Name("func1") - - req, _ := http.NewRequest("GET", "http://localhost/", nil) - context.Set(req, "t", 1) - - res := new(http.ResponseWriter) - r.ServeHTTP(*res, req) - - if _, ok := context.GetOk(req, "t"); ok { - t.Error("Context should have been cleared at end of request") - } - - r.KeepContext = true - - req, _ = http.NewRequest("GET", "http://localhost/", nil) - context.Set(req, "t", 1) - - r.ServeHTTP(*res, req) - if _, ok := context.GetOk(req, "t"); !ok { - t.Error("Context should NOT have been cleared at end of request") - } - -} diff --git a/context_native_test.go b/context_test.go similarity index 97% rename from context_native_test.go rename to context_test.go index c150edf0..d8a56b42 100644 --- a/context_native_test.go +++ b/context_test.go @@ -1,5 +1,3 @@ -// +build go1.7 - package mux import ( From cf6680bc62d81bcd52bbf1c991bfe8b7ec98e270 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sun, 2 Sep 2018 15:36:45 -0700 Subject: [PATCH 39/89] Create release-drafter.yml (#399) --- .github/release-drafter.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..2db2e139 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,8 @@ +# Config for https://github.com/apps/release-drafter +template: | + + + + ## CHANGELOG + + $CHANGES From 9e1f5955c0d22b55d9e20d6faa28589f83b2faca Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Mon, 3 Sep 2018 08:43:05 -0700 Subject: [PATCH 40/89] Always run on the latest stable Go version. (#402) Only run vet on the latest Go version. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1922f6bb..0e58a729 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ matrix: - go: 1.9.x - go: 1.10.x - go: 1.11.x + - go: 1.x + env: LATEST=true - go: tip allow_failures: - go: tip @@ -18,5 +20,5 @@ install: script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d .) - - go tool vet . + - if [[ "$LATEST" = true ]]; then go tool vet .; fi - go test -v -race ./... From deb579d6e030503f430978ee229008b9bc912d40 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 12 Oct 2018 08:31:51 -0700 Subject: [PATCH 41/89] README.md: Update site URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e424397a..0425bb80 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) -http://www.gorillatoolkit.org/pkg/mux +https://www.gorillatoolkit.org/pkg/mux Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to their respective handler. From 521ea7b17d02faf8d3afea6737573942ceac59c5 Mon Sep 17 00:00:00 2001 From: "Nguyen Ngoc Trung (Steven)" Date: Tue, 23 Oct 2018 19:08:00 -0700 Subject: [PATCH 42/89] Use constant for 301 status code in regexp.go (#412) --- regexp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regexp.go b/regexp.go index 2b57e562..b92d59f2 100644 --- a/regexp.go +++ b/regexp.go @@ -296,7 +296,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) } else { u.Path += "/" } - m.Handler = http.RedirectHandler(u.String(), 301) + m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently) } } } From 3d80bc801bb034e17cae38591335b3b1110f1c47 Mon Sep 17 00:00:00 2001 From: kanozec <30459655+kanozec@users.noreply.github.com> Date: Tue, 30 Oct 2018 23:25:28 +0800 Subject: [PATCH 43/89] Use subtests in mux_test.go (#415) --- mux_test.go | 117 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/mux_test.go b/mux_test.go index 31c7d51d..5d4027e4 100644 --- a/mux_test.go +++ b/mux_test.go @@ -205,8 +205,10 @@ func TestHost(t *testing.T) { }, } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -437,10 +439,12 @@ func TestPath(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) - testRegexp(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testUseEscapedRoute(t, test) + testRegexp(t, test) + }) } } @@ -516,9 +520,11 @@ func TestPathPrefix(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testUseEscapedRoute(t, test) + }) } } @@ -623,9 +629,11 @@ func TestSchemeHostPath(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testUseEscapedRoute(t, test) + }) } } @@ -682,8 +690,10 @@ func TestHeaders(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -732,9 +742,11 @@ func TestMethods(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testMethods(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testMethods(t, test) + }) } } @@ -1039,11 +1051,12 @@ func TestQueries(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testQueriesTemplates(t, test) - testUseEscapedRoute(t, test) - testQueriesRegexp(t, test) + t.Run(test.title, func(t *testing.T) { + testTemplate(t, test) + testQueriesTemplates(t, test) + testUseEscapedRoute(t, test) + testQueriesRegexp(t, test) + }) } } @@ -1092,8 +1105,10 @@ func TestSchemes(t *testing.T) { }, } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -1127,8 +1142,10 @@ func TestMatcherFunc(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -1163,8 +1180,10 @@ func TestBuildVarsFunc(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -1294,9 +1313,11 @@ func TestSubRouter(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testUseEscapedRoute(t, test) + }) } } @@ -1400,9 +1421,11 @@ func TestStrictSlash(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) - testUseEscapedRoute(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + testUseEscapedRoute(t, test) + }) } } @@ -1434,8 +1457,10 @@ func TestUseEncodedPath(t *testing.T) { } for _, test := range tests { - testRoute(t, test) - testTemplate(t, test) + t.Run(test.title, func(t *testing.T) { + testRoute(t, test) + testTemplate(t, test) + }) } } @@ -2040,7 +2065,9 @@ func TestMethodsSubrouterCatchall(t *testing.T) { } for _, test := range tests { - testMethodsSubrouter(t, test) + t.Run(test.title, func(t *testing.T) { + testMethodsSubrouter(t, test) + }) } } @@ -2096,7 +2123,9 @@ func TestMethodsSubrouterStrictSlash(t *testing.T) { } for _, test := range tests { - testMethodsSubrouter(t, test) + t.Run(test.title, func(t *testing.T) { + testMethodsSubrouter(t, test) + }) } } @@ -2143,7 +2172,9 @@ func TestMethodsSubrouterPathPrefix(t *testing.T) { } for _, test := range tests { - testMethodsSubrouter(t, test) + t.Run(test.title, func(t *testing.T) { + testMethodsSubrouter(t, test) + }) } } @@ -2199,7 +2230,9 @@ func TestMethodsSubrouterSubrouter(t *testing.T) { } for _, test := range tests { - testMethodsSubrouter(t, test) + t.Run(test.title, func(t *testing.T) { + testMethodsSubrouter(t, test) + }) } } @@ -2253,7 +2286,9 @@ func TestMethodsSubrouterPathVariable(t *testing.T) { } for _, test := range tests { - testMethodsSubrouter(t, test) + t.Run(test.title, func(t *testing.T) { + testMethodsSubrouter(t, test) + }) } } From 758eb64354aa27cda6c1b026822c788cc755f06f Mon Sep 17 00:00:00 2001 From: Joe Wilner Date: Fri, 7 Dec 2018 10:48:26 -0500 Subject: [PATCH 44/89] Improve subroute configuration propagation #422 * Pull out common shared `routeConf` so that config is pushed on to child routers and routes. * Removes obsolete usages of `parentRoute` * Add tests defining compositional behavior * Exercise `copyRouteConf` for posterity --- mux.go | 114 ++++++++------ mux_test.go | 440 +++++++++++++++++++++++++++++++++++++++++++++++++--- regexp.go | 2 +- route.go | 125 ++++----------- 4 files changed, 511 insertions(+), 170 deletions(-) diff --git a/mux.go b/mux.go index 4bbafa51..50ac1184 100644 --- a/mux.go +++ b/mux.go @@ -50,24 +50,77 @@ type Router struct { // Configurable Handler to be used when the request method does not match the route. MethodNotAllowedHandler http.Handler - // Parent route, if this is a subrouter. - parent parentRoute // Routes to be matched, in order. routes []*Route + // Routes by name for URL building. namedRoutes map[string]*Route - // See Router.StrictSlash(). This defines the flag for new routes. - strictSlash bool - // See Router.SkipClean(). This defines the flag for new routes. - skipClean bool + // If true, do not clear the request context after handling the request. // This has no effect when go1.7+ is used, since the context is stored // on the request itself. KeepContext bool - // see Router.UseEncodedPath(). This defines a flag for all routes. - useEncodedPath bool + // Slice of middlewares to be called after a match is found middlewares []middleware + + // configuration shared with `Route` + routeConf +} + +// common route configuration shared between `Router` and `Route` +type routeConf struct { + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + + // Manager for the variables from host and path. + regexp routeRegexpGroup + + // List of matchers. + matchers []matcher + + // The scheme used when building URLs. + buildScheme string + + buildVarsFunc BuildVarsFunc +} + +// returns an effective deep copy of `routeConf` +func copyRouteConf(r routeConf) routeConf { + c := r + + if r.regexp.path != nil { + c.regexp.path = copyRouteRegexp(r.regexp.path) + } + + if r.regexp.host != nil { + c.regexp.host = copyRouteRegexp(r.regexp.host) + } + + c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) + for _, q := range r.regexp.queries { + c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) + } + + c.matchers = make([]matcher, 0, len(r.matchers)) + for _, m := range r.matchers { + c.matchers = append(c.matchers, m) + } + + return c +} + +func copyRouteRegexp(r *routeRegexp) *routeRegexp { + c := *r + return &c } // Match attempts to match the given request against the router's registered routes. @@ -164,13 +217,13 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Get returns a route registered with the given name. func (r *Router) Get(name string) *Route { - return r.getNamedRoutes()[name] + return r.namedRoutes[name] } // GetRoute returns a route registered with the given name. This method // was renamed to Get() and remains here for backwards compatibility. func (r *Router) GetRoute(name string) *Route { - return r.getNamedRoutes()[name] + return r.namedRoutes[name] } // StrictSlash defines the trailing slash behavior for new routes. The initial @@ -221,51 +274,14 @@ func (r *Router) UseEncodedPath() *Router { return r } -// ---------------------------------------------------------------------------- -// parentRoute -// ---------------------------------------------------------------------------- - -func (r *Router) getBuildScheme() string { - if r.parent != nil { - return r.parent.getBuildScheme() - } - return "" -} - -// getNamedRoutes returns the map where named routes are registered. -func (r *Router) getNamedRoutes() map[string]*Route { - if r.namedRoutes == nil { - if r.parent != nil { - r.namedRoutes = r.parent.getNamedRoutes() - } else { - r.namedRoutes = make(map[string]*Route) - } - } - return r.namedRoutes -} - -// getRegexpGroup returns regexp definitions from the parent route, if any. -func (r *Router) getRegexpGroup() *routeRegexpGroup { - if r.parent != nil { - return r.parent.getRegexpGroup() - } - return nil -} - -func (r *Router) buildVars(m map[string]string) map[string]string { - if r.parent != nil { - m = r.parent.buildVars(m) - } - return m -} - // ---------------------------------------------------------------------------- // Route factories // ---------------------------------------------------------------------------- // NewRoute registers an empty route. func (r *Router) NewRoute() *Route { - route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath} + // initialize a route with a copy of the parent router's configuration + route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} r.routes = append(r.routes, route) return route } diff --git a/mux_test.go b/mux_test.go index 5d4027e4..519aa92c 100644 --- a/mux_test.go +++ b/mux_test.go @@ -48,15 +48,6 @@ type routeTest struct { } func TestHost(t *testing.T) { - // newRequestHost a new request with a method, url, and host header - newRequestHost := func(method, url, host string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) - } - req.Host = host - return req - } tests := []routeTest{ { @@ -1193,7 +1184,6 @@ func TestSubRouter(t *testing.T) { subrouter3 := new(Route).PathPrefix("/foo").Subrouter() subrouter4 := new(Route).PathPrefix("/foo/bar").Subrouter() subrouter5 := new(Route).PathPrefix("/{category}").Subrouter() - tests := []routeTest{ { route: subrouter1.Path("/{v2:[a-z]+}"), @@ -1288,6 +1278,106 @@ func TestSubRouter(t *testing.T) { pathTemplate: `/{category}`, shouldMatch: true, }, + { + title: "Mismatch method specified on parent route", + route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), + request: newRequest("GET", "http://localhost/foo/"), + vars: map[string]string{}, + host: "", + path: "/foo/", + pathTemplate: `/foo/`, + shouldMatch: false, + }, + { + title: "Match method specified on parent route", + route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), + request: newRequest("POST", "http://localhost/foo/"), + vars: map[string]string{}, + host: "", + path: "/foo/", + pathTemplate: `/foo/`, + shouldMatch: true, + }, + { + title: "Mismatch scheme specified on parent route", + route: new(Route).Schemes("https").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: false, + }, + { + title: "Match scheme specified on parent route", + route: new(Route).Schemes("http").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: true, + }, + { + title: "No match header specified on parent route", + route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: false, + }, + { + title: "Header mismatch value specified on parent route", + route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), + request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "http"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: false, + }, + { + title: "Header match value specified on parent route", + route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), + request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "https"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: true, + }, + { + title: "Query specified on parent route not present", + route: new(Route).Headers("key", "foobar").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: false, + }, + { + title: "Query mismatch value specified on parent route", + route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/?key=notfoobar"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: false, + }, + { + title: "Query match value specified on subroute", + route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), + request: newRequest("GET", "http://localhost/?key=foobar"), + vars: map[string]string{}, + host: "", + path: "/", + pathTemplate: `/`, + shouldMatch: true, + }, { title: "Build with scheme on parent router", route: new(Route).Schemes("ftp").Host("google.com").Subrouter().Path("/"), @@ -1512,12 +1602,16 @@ func TestWalkSingleDepth(t *testing.T) { func TestWalkNested(t *testing.T) { router := NewRouter() - g := router.Path("/g").Subrouter() - o := g.PathPrefix("/o").Subrouter() - r := o.PathPrefix("/r").Subrouter() - i := r.PathPrefix("/i").Subrouter() - l1 := i.PathPrefix("/l").Subrouter() - l2 := l1.PathPrefix("/l").Subrouter() + routeSubrouter := func(r *Route) (*Route, *Router) { + return r, r.Subrouter() + } + + gRoute, g := routeSubrouter(router.Path("/g")) + oRoute, o := routeSubrouter(g.PathPrefix("/o")) + rRoute, r := routeSubrouter(o.PathPrefix("/r")) + iRoute, i := routeSubrouter(r.PathPrefix("/i")) + l1Route, l1 := routeSubrouter(i.PathPrefix("/l")) + l2Route, l2 := routeSubrouter(l1.PathPrefix("/l")) l2.Path("/a") testCases := []struct { @@ -1525,12 +1619,12 @@ func TestWalkNested(t *testing.T) { ancestors []*Route }{ {"/g", []*Route{}}, - {"/g/o", []*Route{g.parent.(*Route)}}, - {"/g/o/r", []*Route{g.parent.(*Route), o.parent.(*Route)}}, - {"/g/o/r/i", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route)}}, - {"/g/o/r/i/l", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route)}}, - {"/g/o/r/i/l/l", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route), l1.parent.(*Route)}}, - {"/g/o/r/i/l/l/a", []*Route{g.parent.(*Route), o.parent.(*Route), r.parent.(*Route), i.parent.(*Route), l1.parent.(*Route), l2.parent.(*Route)}}, + {"/g/o", []*Route{gRoute}}, + {"/g/o/r", []*Route{gRoute, oRoute}}, + {"/g/o/r/i", []*Route{gRoute, oRoute, rRoute}}, + {"/g/o/r/i/l", []*Route{gRoute, oRoute, rRoute, iRoute}}, + {"/g/o/r/i/l/l", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route}}, + {"/g/o/r/i/l/l/a", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route, l2Route}}, } idx := 0 @@ -1563,8 +1657,8 @@ func TestWalkSubrouters(t *testing.T) { o.Methods("GET") o.Methods("PUT") - // all 4 routes should be matched, but final 2 routes do not have path templates - paths := []string{"/g", "/g/o", "", ""} + // all 4 routes should be matched + paths := []string{"/g", "/g/o", "/g/o", "/g/o"} idx := 0 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { path := paths[idx] @@ -1745,7 +1839,11 @@ func testRoute(t *testing.T, test routeTest) { } } if query != "" { - u, _ := route.URL(mapToPairs(match.Vars)...) + u, err := route.URL(mapToPairs(match.Vars)...) + if err != nil { + t.Errorf("(%v) erred while creating url: %v", test.title, err) + return + } if query != u.RawQuery { t.Errorf("(%v) URL query not equal: expected %v, got %v", test.title, query, u.RawQuery) return @@ -2332,6 +2430,273 @@ func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) { } } +func TestSubrouterMatching(t *testing.T) { + const ( + none, stdOnly, subOnly uint8 = 0, 1 << 0, 1 << 1 + both = subOnly | stdOnly + ) + + type request struct { + Name string + Request *http.Request + Flags uint8 + } + + cases := []struct { + Name string + Standard, Subrouter func(*Router) + Requests []request + }{ + { + "pathPrefix", + func(r *Router) { + r.PathPrefix("/before").PathPrefix("/after") + }, + func(r *Router) { + r.PathPrefix("/before").Subrouter().PathPrefix("/after") + }, + []request{ + {"no match final path prefix", newRequest("GET", "/after"), none}, + {"no match parent path prefix", newRequest("GET", "/before"), none}, + {"matches append", newRequest("GET", "/before/after"), both}, + {"matches as prefix", newRequest("GET", "/before/after/1234"), both}, + }, + }, + { + "path", + func(r *Router) { + r.Path("/before").Path("/after") + }, + func(r *Router) { + r.Path("/before").Subrouter().Path("/after") + }, + []request{ + {"no match subroute path", newRequest("GET", "/after"), none}, + {"no match parent path", newRequest("GET", "/before"), none}, + {"no match as prefix", newRequest("GET", "/before/after/1234"), none}, + {"no match append", newRequest("GET", "/before/after"), none}, + }, + }, + { + "host", + func(r *Router) { + r.Host("before.com").Host("after.com") + }, + func(r *Router) { + r.Host("before.com").Subrouter().Host("after.com") + }, + []request{ + {"no match before", newRequestHost("GET", "/", "before.com"), none}, + {"no match other", newRequestHost("GET", "/", "other.com"), none}, + {"matches after", newRequestHost("GET", "/", "after.com"), none}, + }, + }, + { + "queries variant keys", + func(r *Router) { + r.Queries("foo", "bar").Queries("cricket", "baseball") + }, + func(r *Router) { + r.Queries("foo", "bar").Subrouter().Queries("cricket", "baseball") + }, + []request{ + {"matches with all", newRequest("GET", "/?foo=bar&cricket=baseball"), both}, + {"matches with more", newRequest("GET", "/?foo=bar&cricket=baseball&something=else"), both}, + {"no match with none", newRequest("GET", "/"), none}, + {"no match with some", newRequest("GET", "/?cricket=baseball"), none}, + }, + }, + { + "queries overlapping keys", + func(r *Router) { + r.Queries("foo", "bar").Queries("foo", "baz") + }, + func(r *Router) { + r.Queries("foo", "bar").Subrouter().Queries("foo", "baz") + }, + []request{ + {"no match old value", newRequest("GET", "/?foo=bar"), none}, + {"no match diff value", newRequest("GET", "/?foo=bak"), none}, + {"no match with none", newRequest("GET", "/"), none}, + {"matches override", newRequest("GET", "/?foo=baz"), none}, + }, + }, + { + "header variant keys", + func(r *Router) { + r.Headers("foo", "bar").Headers("cricket", "baseball") + }, + func(r *Router) { + r.Headers("foo", "bar").Subrouter().Headers("cricket", "baseball") + }, + []request{ + { + "matches with all", + newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball"), + both, + }, + { + "matches with more", + newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball", "something", "else"), + both, + }, + {"no match with none", newRequest("GET", "/"), none}, + {"no match with some", newRequestWithHeaders("GET", "/", "cricket", "baseball"), none}, + }, + }, + { + "header overlapping keys", + func(r *Router) { + r.Headers("foo", "bar").Headers("foo", "baz") + }, + func(r *Router) { + r.Headers("foo", "bar").Subrouter().Headers("foo", "baz") + }, + []request{ + {"no match old value", newRequestWithHeaders("GET", "/", "foo", "bar"), none}, + {"no match diff value", newRequestWithHeaders("GET", "/", "foo", "bak"), none}, + {"no match with none", newRequest("GET", "/"), none}, + {"matches override", newRequestWithHeaders("GET", "/", "foo", "baz"), none}, + }, + }, + { + "method", + func(r *Router) { + r.Methods("POST").Methods("GET") + }, + func(r *Router) { + r.Methods("POST").Subrouter().Methods("GET") + }, + []request{ + {"matches before", newRequest("POST", "/"), none}, + {"no match other", newRequest("HEAD", "/"), none}, + {"matches override", newRequest("GET", "/"), none}, + }, + }, + { + "schemes", + func(r *Router) { + r.Schemes("http").Schemes("https") + }, + func(r *Router) { + r.Schemes("http").Subrouter().Schemes("https") + }, + []request{ + {"matches overrides", newRequest("GET", "https://www.example.com/"), none}, + {"matches original", newRequest("GET", "http://www.example.com/"), none}, + {"no match other", newRequest("GET", "ftp://www.example.com/"), none}, + }, + }, + } + + // case -> request -> router + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + for _, req := range c.Requests { + t.Run(req.Name, func(t *testing.T) { + for _, v := range []struct { + Name string + Config func(*Router) + Expected bool + }{ + {"subrouter", c.Subrouter, (req.Flags & subOnly) != 0}, + {"standard", c.Standard, (req.Flags & stdOnly) != 0}, + } { + r := NewRouter() + v.Config(r) + if r.Match(req.Request, &RouteMatch{}) != v.Expected { + if v.Expected { + t.Errorf("expected %v match", v.Name) + } else { + t.Errorf("expected %v no match", v.Name) + } + } + } + }) + } + }) + } +} + +// verify that copyRouteConf copies fields as expected. +func Test_copyRouteConf(t *testing.T) { + var ( + m MatcherFunc = func(*http.Request, *RouteMatch) bool { + return true + } + b BuildVarsFunc = func(i map[string]string) map[string]string { + return i + } + r, _ = newRouteRegexp("hi", regexpTypeHost, routeRegexpOptions{}) + ) + + tests := []struct { + name string + args routeConf + want routeConf + }{ + { + "empty", + routeConf{}, + routeConf{}, + }, + { + "full", + routeConf{ + useEncodedPath: true, + strictSlash: true, + skipClean: true, + regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, + matchers: []matcher{m}, + buildScheme: "https", + buildVarsFunc: b, + }, + routeConf{ + useEncodedPath: true, + strictSlash: true, + skipClean: true, + regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, + matchers: []matcher{m}, + buildScheme: "https", + buildVarsFunc: b, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // special case some incomparable fields of routeConf before delegating to reflect.DeepEqual + got := copyRouteConf(tt.args) + + // funcs not comparable, just compare length of slices + if len(got.matchers) != len(tt.want.matchers) { + t.Errorf("matchers different lengths: %v %v", len(got.matchers), len(tt.want.matchers)) + } + got.matchers, tt.want.matchers = nil, nil + + // deep equal treats nil slice differently to empty slice so check for zero len first + { + bothZero := len(got.regexp.queries) == 0 && len(tt.want.regexp.queries) == 0 + if !bothZero && !reflect.DeepEqual(got.regexp.queries, tt.want.regexp.queries) { + t.Errorf("queries unequal: %v %v", got.regexp.queries, tt.want.regexp.queries) + } + got.regexp.queries, tt.want.regexp.queries = nil, nil + } + + // funcs not comparable, just compare nullity + if (got.buildVarsFunc == nil) != (tt.want.buildVarsFunc == nil) { + t.Errorf("build vars funcs unequal: %v %v", got.buildVarsFunc == nil, tt.want.buildVarsFunc == nil) + } + got.buildVarsFunc, tt.want.buildVarsFunc = nil, nil + + // finish the deal + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("route confs unequal: %v %v", got, tt.want) + } + }) + } +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int @@ -2406,3 +2771,28 @@ func newRequest(method, url string) *http.Request { } return req } + +// create a new request with the provided headers +func newRequestWithHeaders(method, url string, headers ...string) *http.Request { + req := newRequest(method, url) + + if len(headers)%2 != 0 { + panic(fmt.Sprintf("Expected headers length divisible by 2 but got %v", len(headers))) + } + + for i := 0; i < len(headers); i += 2 { + req.Header.Set(headers[i], headers[i+1]) + } + + return req +} + +// newRequestHost a new request with a method, url, and host header +func newRequestHost(method, url, host string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + req.Host = host + return req +} diff --git a/regexp.go b/regexp.go index b92d59f2..7c7405d1 100644 --- a/regexp.go +++ b/regexp.go @@ -267,7 +267,7 @@ type routeRegexpGroup struct { } // setMatch extracts the variables from the URL once a route matches. -func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { +func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { // Store host variables. if v.host != nil { host := getHost(req) diff --git a/route.go b/route.go index c8bb5c7e..acef9195 100644 --- a/route.go +++ b/route.go @@ -15,24 +15,8 @@ import ( // Route stores information to match a request and build URLs. type Route struct { - // Parent where the route was registered (a Router). - parent parentRoute // Request handler for the route. handler http.Handler - // List of matchers. - matchers []matcher - // Manager for the variables from host and path. - regexp *routeRegexpGroup - // If true, when the path pattern is "/path/", accessing "/path" will - // redirect to the former and vice versa. - strictSlash bool - // If true, when the path pattern is "/path//to", accessing "/path//to" - // will not redirect - skipClean bool - // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" - useEncodedPath bool - // The scheme used when building URLs. - buildScheme string // If true, this route never matches: it is only used to build URLs. buildOnly bool // The name used to build URLs. @@ -40,7 +24,11 @@ type Route struct { // Error resulted from building a route. err error - buildVarsFunc BuildVarsFunc + // "global" reference to all named routes + namedRoutes map[string]*Route + + // config possibly passed in from `Router` + routeConf } // SkipClean reports whether path cleaning is enabled for this route via @@ -93,9 +81,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { } // Set variables. - if r.regexp != nil { - r.regexp.setMatch(req, match, r) - } + r.regexp.setMatch(req, match, r) return true } @@ -145,7 +131,7 @@ func (r *Route) Name(name string) *Route { } if r.err == nil { r.name = name - r.getNamedRoutes()[name] = r + r.namedRoutes[name] = r } return r } @@ -177,7 +163,6 @@ func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { if r.err != nil { return r.err } - r.regexp = r.getRegexpGroup() if typ == regexpTypePath || typ == regexpTypePrefix { if len(tpl) > 0 && tpl[0] != '/' { return fmt.Errorf("mux: path must start with a slash, got %q", tpl) @@ -424,7 +409,7 @@ func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) } - if r.buildScheme == "" && len(schemes) > 0 { + if len(schemes) > 0 { r.buildScheme = schemes[0] } return r.addMatcher(schemeMatcher(schemes)) @@ -439,7 +424,15 @@ type BuildVarsFunc func(map[string]string) map[string]string // BuildVarsFunc adds a custom function to be used to modify build variables // before a route's URL is built. func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { - r.buildVarsFunc = f + if r.buildVarsFunc != nil { + // compose the old and new functions + old := r.buildVarsFunc + r.buildVarsFunc = func(m map[string]string) map[string]string { + return f(old(m)) + } + } else { + r.buildVarsFunc = f + } return r } @@ -458,7 +451,8 @@ func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { // Here, the routes registered in the subrouter won't be tested if the host // doesn't match. func (r *Route) Subrouter() *Router { - router := &Router{parent: r, strictSlash: r.strictSlash} + // initialize a subrouter with a copy of the parent route's configuration + router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} r.addMatcher(router) return router } @@ -502,9 +496,6 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { if r.err != nil { return nil, r.err } - if r.regexp == nil { - return nil, errors.New("mux: route doesn't have a host or path") - } values, err := r.prepareVars(pairs...) if err != nil { return nil, err @@ -516,8 +507,8 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { return nil, err } scheme = "http" - if s := r.getBuildScheme(); s != "" { - scheme = s + if r.buildScheme != "" { + scheme = r.buildScheme } } if r.regexp.path != nil { @@ -547,7 +538,7 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.host == nil { + if r.regexp.host == nil { return nil, errors.New("mux: route doesn't have a host") } values, err := r.prepareVars(pairs...) @@ -562,8 +553,8 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { Scheme: "http", Host: host, } - if s := r.getBuildScheme(); s != "" { - u.Scheme = s + if r.buildScheme != "" { + u.Scheme = r.buildScheme } return u, nil } @@ -575,7 +566,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.path == nil { + if r.regexp.path == nil { return nil, errors.New("mux: route doesn't have a path") } values, err := r.prepareVars(pairs...) @@ -600,7 +591,7 @@ func (r *Route) GetPathTemplate() (string, error) { if r.err != nil { return "", r.err } - if r.regexp == nil || r.regexp.path == nil { + if r.regexp.path == nil { return "", errors.New("mux: route doesn't have a path") } return r.regexp.path.template, nil @@ -614,7 +605,7 @@ func (r *Route) GetPathRegexp() (string, error) { if r.err != nil { return "", r.err } - if r.regexp == nil || r.regexp.path == nil { + if r.regexp.path == nil { return "", errors.New("mux: route does not have a path") } return r.regexp.path.regexp.String(), nil @@ -629,7 +620,7 @@ func (r *Route) GetQueriesRegexp() ([]string, error) { if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.queries == nil { + if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } var queries []string @@ -648,7 +639,7 @@ func (r *Route) GetQueriesTemplates() ([]string, error) { if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.queries == nil { + if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } var queries []string @@ -683,7 +674,7 @@ func (r *Route) GetHostTemplate() (string, error) { if r.err != nil { return "", r.err } - if r.regexp == nil || r.regexp.host == nil { + if r.regexp.host == nil { return "", errors.New("mux: route doesn't have a host") } return r.regexp.host.template, nil @@ -700,64 +691,8 @@ func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { } func (r *Route) buildVars(m map[string]string) map[string]string { - if r.parent != nil { - m = r.parent.buildVars(m) - } if r.buildVarsFunc != nil { m = r.buildVarsFunc(m) } return m } - -// ---------------------------------------------------------------------------- -// parentRoute -// ---------------------------------------------------------------------------- - -// parentRoute allows routes to know about parent host and path definitions. -type parentRoute interface { - getBuildScheme() string - getNamedRoutes() map[string]*Route - getRegexpGroup() *routeRegexpGroup - buildVars(map[string]string) map[string]string -} - -func (r *Route) getBuildScheme() string { - if r.buildScheme != "" { - return r.buildScheme - } - if r.parent != nil { - return r.parent.getBuildScheme() - } - return "" -} - -// getNamedRoutes returns the map where named routes are registered. -func (r *Route) getNamedRoutes() map[string]*Route { - if r.parent == nil { - // During tests router is not always set. - r.parent = NewRouter() - } - return r.parent.getNamedRoutes() -} - -// getRegexpGroup returns regexp definitions from this route. -func (r *Route) getRegexpGroup() *routeRegexpGroup { - if r.regexp == nil { - if r.parent == nil { - // During tests router is not always set. - r.parent = NewRouter() - } - regexp := r.parent.getRegexpGroup() - if regexp == nil { - r.regexp = new(routeRegexpGroup) - } else { - // Copy. - r.regexp = &routeRegexpGroup{ - host: regexp.host, - path: regexp.path, - queries: regexp.queries, - } - } - } - return r.regexp -} From 419fd9fe2a07c30b927f0183d6a99408d187b812 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 7 Dec 2018 10:41:48 -0600 Subject: [PATCH 45/89] Add stalebot config (#424) --- .github/stale | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/stale diff --git a/.github/stale b/.github/stale new file mode 100644 index 00000000..f5c1622a --- /dev/null +++ b/.github/stale @@ -0,0 +1,11 @@ +daysUntilStale: 60 +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - v2 + - needs-review +staleLabel: stale +markComment: > + This issue has been automatically marked as stale because it hasn't seen + a recent update. It'll be automatically closed in a few days. +closeComment: false From d2b5d13b9260193b5a5649e9f47fda645bd2bd1d Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 8 Dec 2018 12:40:53 -0800 Subject: [PATCH 46/89] Update and rename stale to stale.yml (#425) --- .github/{stale => stale.yml} | 1 + 1 file changed, 1 insertion(+) rename .github/{stale => stale.yml} (94%) diff --git a/.github/stale b/.github/stale.yml similarity index 94% rename from .github/stale rename to .github/stale.yml index f5c1622a..de8a6780 100644 --- a/.github/stale +++ b/.github/stale.yml @@ -4,6 +4,7 @@ daysUntilClose: 7 exemptLabels: - v2 - needs-review + - work-required staleLabel: stale markComment: > This issue has been automatically marked as stale because it hasn't seen From 6137e193cdcba2725f16de84eaf0db2b769d9668 Mon Sep 17 00:00:00 2001 From: Michael Li Date: Mon, 17 Dec 2018 22:42:43 +0800 Subject: [PATCH 47/89] remove redundant code that remove support gorilla/context (#427) * remove redundant code that remove support gorilla/context * backward compatible for remove redundant code --- context.go | 4 ---- mux.go | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/context.go b/context.go index 13a4601a..665940a2 100644 --- a/context.go +++ b/context.go @@ -16,7 +16,3 @@ func contextSet(r *http.Request, key, val interface{}) *http.Request { return r.WithContext(context.WithValue(r.Context(), key, val)) } - -func contextClear(r *http.Request) { - return -} diff --git a/mux.go b/mux.go index 50ac1184..8aca972d 100644 --- a/mux.go +++ b/mux.go @@ -22,7 +22,7 @@ var ( // NewRouter returns a new router instance. func NewRouter() *Router { - return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} + return &Router{namedRoutes: make(map[string]*Route)} } // Router registers routes to be matched and dispatches a handler. @@ -57,7 +57,8 @@ type Router struct { namedRoutes map[string]*Route // If true, do not clear the request context after handling the request. - // This has no effect when go1.7+ is used, since the context is stored + // + // Deprecated: No effect when go1.7+ is used, since the context is stored // on the request itself. KeepContext bool @@ -208,10 +209,6 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { handler = http.NotFoundHandler() } - if !r.KeepContext { - defer contextClear(req) - } - handler.ServeHTTP(w, req) } From a31c1782bfb10b7b3799d5ec06b5ccbd98c4ec7e Mon Sep 17 00:00:00 2001 From: Raees Date: Tue, 25 Dec 2018 21:41:17 +0500 Subject: [PATCH 48/89] Replace domain.com with example.com (#434) Because domain.com is an actual business, example.com should be used for example purposes. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0425bb80..9e99a270 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ r := mux.NewRouter() // Only matches if domain is "www.example.com". r.Host("www.example.com") // Matches a dynamic subdomain. -r.Host("{subdomain:[a-z]+}.domain.com") +r.Host("{subdomain:[a-z]+}.example.com") ``` There are several other matchers that can be added. To match path prefixes: @@ -238,13 +238,13 @@ This also works for host and query value variables: ```go r := mux.NewRouter() -r.Host("{subdomain}.domain.com"). +r.Host("{subdomain}.example.com"). Path("/articles/{category}/{id:[0-9]+}"). Queries("filter", "{filter}"). HandlerFunc(ArticleHandler). Name("article") -// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" +// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42", @@ -264,7 +264,7 @@ r.HeadersRegexp("Content-Type", "application/(text|json)") There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: ```go -// "http://news.domain.com/" +// "http://news.example.com/" host, err := r.Get("article").URLHost("subdomain", "news") // "/articles/technology/42" @@ -275,12 +275,12 @@ And if you use subrouters, host and path defined separately can be built as well ```go r := mux.NewRouter() -s := r.Host("{subdomain}.domain.com").Subrouter() +s := r.Host("{subdomain}.example.com").Subrouter() s.Path("/articles/{category}/{id:[0-9]+}"). HandlerFunc(ArticleHandler). Name("article") -// "http://news.domain.com/articles/technology/42" +// "http://news.example.com/articles/technology/42" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42") From ef912dd76ebe9d9848c6e0fd80eaebccc9a11631 Mon Sep 17 00:00:00 2001 From: tomare Date: Thu, 27 Dec 2018 19:42:16 -0500 Subject: [PATCH 49/89] [bugfix] Clear matchErr when traversing subrouters. Previously, when searching for a match, matchErr would be erroneously set, and prevent middleware from running (no match == no middleware runs). This fix clears matchErr before traversing the next subrouter in a multi-subrouter router. --- middleware_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++ route.go | 5 ++++ 2 files changed, 65 insertions(+) diff --git a/middleware_test.go b/middleware_test.go index acf4e160..b708be09 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -375,3 +375,63 @@ func TestCORSMethodMiddleware(t *testing.T) { } } } + +func TestMiddlewareOnMultiSubrouter(t *testing.T) { + first := "first" + second := "second" + notFound := "404 not found" + + router := NewRouter() + firstSubRouter := router.PathPrefix("/").Subrouter() + secondSubRouter := router.PathPrefix("/").Subrouter() + + router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(notFound)) + }) + + firstSubRouter.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { + + }) + + secondSubRouter.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { + + }) + + firstSubRouter.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(first)) + h.ServeHTTP(w, r) + }) + }) + + secondSubRouter.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(second)) + h.ServeHTTP(w, r) + }) + }) + + rw := NewRecorder() + req := newRequest("GET", "/first") + + router.ServeHTTP(rw, req) + if rw.Body.String() != first { + t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String()) + } + + rw = NewRecorder() + req = newRequest("GET", "/second") + + router.ServeHTTP(rw, req) + if rw.Body.String() != second { + t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String()) + } + + rw = NewRecorder() + req = newRequest("GET", "/second/not-exist") + + router.ServeHTTP(rw, req) + if rw.Body.String() != notFound { + t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String()) + } +} diff --git a/route.go b/route.go index acef9195..a1970966 100644 --- a/route.go +++ b/route.go @@ -43,6 +43,11 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { return false } + // Set MatchErr to nil to prevent + // subsequent matching subrouters from failing to run middleware. + // If not reset, the middleware would see a non-nil MatchErr and be skipped, + // even when there was a matching route. + match.MatchErr = nil var matchErr error // Match everything. From f3ff42f93a451d7ffb2ff11cb9485f3f88089c83 Mon Sep 17 00:00:00 2001 From: santsai Date: Fri, 4 Jan 2019 23:08:45 +0800 Subject: [PATCH 50/89] getHost() now returns full host & port information (#383) Previously, getHost only returned the host. As it now returns the port as well, any .Host matches on a route will need to be updated to also support matching on the port for cases where the port is non default, eg: 80 for http or 443 for https. --- mux_test.go | 20 +++++++++++++++++++- regexp.go | 10 +++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mux_test.go b/mux_test.go index 519aa92c..8ad57ac8 100644 --- a/mux_test.go +++ b/mux_test.go @@ -104,7 +104,15 @@ func TestHost(t *testing.T) { path: "", shouldMatch: false, }, - // BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true}, + { + title: "Host route with port, match with request header", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + shouldMatch: true, + }, { title: "Host route with port, wrong host in request header", route: new(Route).Host("aaa.bbb.ccc:1234"), @@ -114,6 +122,16 @@ func TestHost(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Host route with pattern, match with request header", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:123"), + vars: map[string]string{"v1": "bbb", "v2": "23"}, + host: "aaa.bbb.ccc:123", + path: "", + hostTemplate: `aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}`, + shouldMatch: true, + }, { title: "Host route with pattern, match", route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), diff --git a/regexp.go b/regexp.go index 7c7405d1..f2528867 100644 --- a/regexp.go +++ b/regexp.go @@ -312,17 +312,13 @@ func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { } // getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. func getHost(r *http.Request) string { if r.URL.IsAbs() { return r.URL.Host } - host := r.Host - // Slice off any port information. - if i := strings.Index(host, ":"); i != -1 { - host = host[:i] - } - return host - + return r.Host } func extractVars(input string, matches []int, names []string, output map[string]string) { From 08e7f807d38d6a870193019bb439056118661505 Mon Sep 17 00:00:00 2001 From: Gregor Weckbecker Date: Tue, 8 Jan 2019 15:29:30 +0100 Subject: [PATCH 51/89] Ignore ErrNotFound while matching Subrouters (#438) MatchErr is set by the router to ErrNotFound if no route matches. If no route of a Subrouter matches the error can by safely ignored. This implementation only ignores these errors and does not ignore other errors like ErrMethodMismatch. --- mux_test.go | 32 ++++++++++++++++++++++++++++++++ route.go | 17 ++++++++++++----- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/mux_test.go b/mux_test.go index 8ad57ac8..5198024d 100644 --- a/mux_test.go +++ b/mux_test.go @@ -2715,6 +2715,38 @@ func Test_copyRouteConf(t *testing.T) { } } +func TestMethodNotAllowed(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + router := NewRouter() + router.HandleFunc("/thing", handler).Methods(http.MethodGet) + router.HandleFunc("/something", handler).Methods(http.MethodGet) + + w := NewRecorder() + req := newRequest(http.MethodPut, "/thing") + + router.ServeHTTP(w, req) + + if w.Code != 405 { + t.Fatalf("Expected status code 405 (got %d)", w.Code) + } +} + +func TestSubrouterNotFound(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + router := NewRouter() + router.Path("/a").Subrouter().HandleFunc("/thing", handler).Methods(http.MethodGet) + router.Path("/b").Subrouter().HandleFunc("/something", handler).Methods(http.MethodGet) + + w := NewRecorder() + req := newRequest(http.MethodPut, "/not-present") + + router.ServeHTTP(w, req) + + if w.Code != 404 { + t.Fatalf("Expected status code 404 (got %d)", w.Code) + } +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int diff --git a/route.go b/route.go index a1970966..16a7cdf4 100644 --- a/route.go +++ b/route.go @@ -43,11 +43,6 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { return false } - // Set MatchErr to nil to prevent - // subsequent matching subrouters from failing to run middleware. - // If not reset, the middleware would see a non-nil MatchErr and be skipped, - // even when there was a matching route. - match.MatchErr = nil var matchErr error // Match everything. @@ -57,6 +52,18 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { matchErr = ErrMethodMismatch continue } + + // Ignore ErrNotFound errors. These errors arise from match call + // to Subrouters. + // + // This prevents subsequent matching subrouters from failing to + // run middleware. If not ignored, the middleware would see a + // non-nil MatchErr and be skipped, even when there was a + // matching route. + if match.MatchErr == ErrNotFound { + match.MatchErr = nil + } + matchErr = nil return false } From 797e653da60e4619dcb0ae703714147b5a7454c6 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 25 Jan 2019 11:41:49 +0000 Subject: [PATCH 52/89] Call WriteHeader after setting other header(s) in the example (#442) From the docs: Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e99a270..c661599a 100644 --- a/README.md +++ b/README.md @@ -503,8 +503,8 @@ package main func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { // A very simple health check. - w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) // In the future we could report back on the status of our DB, or our cache // (e.g. Redis) by performing a simple PING, and include them in the response. From a7962380ca08b5a188038c69871b8d3fbdf31e89 Mon Sep 17 00:00:00 2001 From: moeryomenko Date: Fri, 25 Jan 2019 19:05:53 +0300 Subject: [PATCH 53/89] replace rr.HeaderMap by rr.Header() (#443) --- middleware_test.go | 6 +++--- mux_test.go | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/middleware_test.go b/middleware_test.go index b708be09..24016cbb 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -151,7 +151,7 @@ func TestMiddlewareExecution(t *testing.T) { // Test handler-only call router.ServeHTTP(rw, req) - if bytes.Compare(rw.Body.Bytes(), handlerStr) != 0 { + if !bytes.Equal(rw.Body.Bytes(), handlerStr) { t.Fatal("Handler response is not what it should be") } @@ -166,7 +166,7 @@ func TestMiddlewareExecution(t *testing.T) { }) router.ServeHTTP(rw, req) - if bytes.Compare(rw.Body.Bytes(), append(mwStr, handlerStr...)) != 0 { + if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) { t.Fatal("Middleware + handler response is not what it should be") } } @@ -368,7 +368,7 @@ func TestCORSMethodMiddleware(t *testing.T) { t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String()) } - allowedMethods := rr.HeaderMap.Get("Access-Control-Allow-Methods") + allowedMethods := rr.Header().Get("Access-Control-Allow-Methods") if allowedMethods != tt.expectedAllowedMethods { t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods) diff --git a/mux_test.go b/mux_test.go index 5198024d..61653d7f 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1123,10 +1123,7 @@ func TestSchemes(t *testing.T) { func TestMatcherFunc(t *testing.T) { m := func(r *http.Request, m *RouteMatch) bool { - if r.URL.Host == "aaa.bbb.ccc" { - return true - } - return false + return r.URL.Host == "aaa.bbb.ccc" } tests := []routeTest{ From 8559a4f775fc329165fe32bd4c2543de8ada8fce Mon Sep 17 00:00:00 2001 From: Souvik Haldar Date: Sun, 17 Feb 2019 21:08:49 +0530 Subject: [PATCH 54/89] [docs] typo (#454) --- route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route.go b/route.go index 16a7cdf4..8479c68c 100644 --- a/route.go +++ b/route.go @@ -383,7 +383,7 @@ func (r *Route) PathPrefix(tpl string) *Route { // The above route will only match if the URL contains the defined queries // values, e.g.: ?foo=bar&id=42. // -// It the value is an empty string, it will match any value if the key is set. +// If the value is an empty string, it will match any value if the key is set. // // Variables can define an optional regexp pattern to be matched: // From 8eaa9f13091105874ef3e20c65922e382cef3c64 Mon Sep 17 00:00:00 2001 From: Benjamin Boudreau Date: Thu, 28 Feb 2019 12:36:07 -0500 Subject: [PATCH 55/89] fix go1.12 go vet usage (#458) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0e58a729..a34d6c49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,5 @@ install: script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d .) - - if [[ "$LATEST" = true ]]; then go tool vet .; fi + - if [[ "$LATEST" = true ]]; then go vet .; fi - go test -v -race ./... From 15a353a636720571d19e37b34a14499c3afa9991 Mon Sep 17 00:00:00 2001 From: Benjamin Boudreau Date: Thu, 28 Feb 2019 13:12:03 -0500 Subject: [PATCH 56/89] adding Router.Name to create new Route (#457) --- mux.go | 6 ++++++ mux_test.go | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mux.go b/mux.go index 8aca972d..a2cd193e 100644 --- a/mux.go +++ b/mux.go @@ -283,6 +283,12 @@ func (r *Router) NewRoute() *Route { return route } +// Name registers a new route with a name. +// See Route.Name(). +func (r *Router) Name(name string) *Route { + return r.NewRoute().Name(name) +} + // Handle registers a new route with a matcher for the URL path. // See Route.Path() and Route.Handler(). func (r *Router) Handle(path string, handler http.Handler) *Route { diff --git a/mux_test.go b/mux_test.go index 61653d7f..f5c1e9c5 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1441,10 +1441,11 @@ func TestNamedRoutes(t *testing.T) { r3.NewRoute().Name("g") r3.NewRoute().Name("h") r3.NewRoute().Name("i") + r3.Name("j") - if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 { - t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes) - } else if r1.Get("i") == nil { + if r1.namedRoutes == nil || len(r1.namedRoutes) != 10 { + t.Errorf("Expected 10 named routes, got %v", r1.namedRoutes) + } else if r1.Get("j") == nil { t.Errorf("Subroute name not registered") } } From c5c6c98bc25355028a63748a498942a6398ccd22 Mon Sep 17 00:00:00 2001 From: sekky0905 <20237968+sekky0905@users.noreply.github.com> Date: Sat, 16 Mar 2019 22:32:43 +0900 Subject: [PATCH 57/89] [build] Remove sudo setting from travis.yml (#462) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a34d6c49..d003ad92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: go -sudo: false + matrix: include: From ed099d42384823742bba0bf9a72b53b55c9e2e38 Mon Sep 17 00:00:00 2001 From: "M@" Date: Thu, 16 May 2019 20:20:44 -0400 Subject: [PATCH 58/89] host:port matching does not require a :port to be specified. In lieu of checking the template pattern on every Match request, a bool is added to the routeRegexp, and set if the routeRegexp is a host AND there is no ":" in the template. I dislike extending the type, but I'd dislike doing a string match on every single Match, even more. --- regexp.go | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/regexp.go b/regexp.go index f2528867..ac1abcd4 100644 --- a/regexp.go +++ b/regexp.go @@ -113,6 +113,13 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro if typ != regexpTypePrefix { pattern.WriteByte('$') } + + var wildcardHostPort bool + if typ == regexpTypeHost { + if !strings.Contains(pattern.String(), ":") { + wildcardHostPort = true + } + } reverse.WriteString(raw) if endSlash { reverse.WriteByte('/') @@ -131,13 +138,14 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro // Done! return &routeRegexp{ - template: template, - regexpType: typ, - options: options, - regexp: reg, - reverse: reverse.String(), - varsN: varsN, - varsR: varsR, + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + wildcardHostPort: wildcardHostPort, }, nil } @@ -158,11 +166,22 @@ type routeRegexp struct { varsN []string // Variable regexps (validators). varsR []*regexp.Regexp + // Wildcard host-port (no strict port match in hostname) + wildcardHostPort bool } // Match matches the regexp against the URL host or path. func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { - if r.regexpType != regexpTypeHost { + if r.regexpType == regexpTypeHost { + host := getHost(req) + if r.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + return r.regexp.MatchString(host) + } else { if r.regexpType == regexpTypeQuery { return r.matchQueryString(req) } @@ -172,8 +191,6 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { } return r.regexp.MatchString(path) } - - return r.regexp.MatchString(getHost(req)) } // url builds a URL part using the given values. From 212aa90d7cec051ab29930d5c56f758f6f69a789 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Mon, 24 Jun 2019 09:05:39 -0700 Subject: [PATCH 59/89] [WIP] Create CircleCI config (#484) * [ci] Create CircleCI config * Fix typos in container versions * Add CircleCI badge --- .circleci/config.yml | 63 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 64 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..c0fb9de3 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,63 @@ +version: 2.0 + +jobs: + # Base test configuration for Go library tests Each distinct version should + # inherit this base, and override (at least) the container image used. + "test": &test + docker: + - image: circleci/golang:latest + working_directory: /go/src/github.com/gorilla/mux + steps: &steps + - checkout + - run: go version + - run: go get -t -v ./... + - run: diff -u <(echo -n) <(gofmt -d .) + - run: if [[ "$LATEST" = true ]]; then go vet -v .; fi + - run: go test -v -race ./... + + "latest": + <<: *test + environment: + LATEST: true + + "1.12": + <<: *test + docker: + - image: circleci/golang:1.12 + + "1.11": + <<: *test + docker: + - image: circleci/golang:1.11 + + "1.10": + <<: *test + docker: + - image: circleci/golang:1.10 + + "1.9": + <<: *test + docker: + - image: circleci/golang:1.9 + + "1.8": + <<: *test + docker: + - image: circleci/golang:1.8 + + "1.7": + <<: *test + docker: + - image: circleci/golang:1.7 + +workflows: + version: 2 + build: + jobs: + - "latest" + - "1.12" + - "1.11" + - "1.10" + - "1.9" + - "1.8" + - "1.7" diff --git a/README.md b/README.md index c661599a..08f63a10 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) +[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) ![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) From 4248f5cd8717eaea35eded08100714b2b2bac756 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Fri, 28 Jun 2019 08:33:07 -0700 Subject: [PATCH 60/89] Fix nil panic in authentication middleware example (#489) --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 38957dee..bd5a38b5 100644 --- a/doc.go +++ b/doc.go @@ -295,7 +295,7 @@ A more complex authentication middleware, which maps session token to users, cou r := mux.NewRouter() r.HandleFunc("/", handler) - amw := authenticationMiddleware{} + amw := authenticationMiddleware{tokenUsers: make(map[string]string)} amw.Populate() r.Use(amw.Middleware) From 64954673e972292a409b5c8d13b16ed797b905d0 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 28 Jun 2019 16:07:30 -0700 Subject: [PATCH 61/89] Delete .travis.yml (#490) --- .travis.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d003ad92..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: go - - -matrix: - include: - - go: 1.7.x - - go: 1.8.x - - go: 1.9.x - - go: 1.10.x - - go: 1.11.x - - go: 1.x - env: LATEST=true - - go: tip - allow_failures: - - go: tip - -install: - - # Skip - -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d .) - - if [[ "$LATEST" = true ]]; then go vet .; fi - - go test -v -race ./... From 48f941fa99947e50375f2b0dd4b3b060d15b2fe8 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Sat, 29 Jun 2019 10:24:12 -0700 Subject: [PATCH 62/89] Use subtests for middleware tests (#478) * Use subtests for middleware tests * Don't use subtests for MiddlewareAdd --- middleware_test.go | 382 +++++++++++++++++++++++++-------------------- 1 file changed, 210 insertions(+), 172 deletions(-) diff --git a/middleware_test.go b/middleware_test.go index 24016cbb..30df2746 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -2,6 +2,7 @@ package mux import ( "bytes" + "fmt" "net/http" "net/http/httptest" "testing" @@ -28,12 +29,12 @@ func TestMiddlewareAdd(t *testing.T) { router.useInterface(mw) if len(router.middlewares) != 1 || router.middlewares[0] != mw { - t.Fatal("Middleware was not added correctly") + t.Fatal("Middleware interface was not added correctly") } router.Use(mw.Middleware) if len(router.middlewares) != 2 { - t.Fatal("MiddlewareFunc method was not added correctly") + t.Fatal("Middleware method was not added correctly") } banalMw := func(handler http.Handler) http.Handler { @@ -41,7 +42,7 @@ func TestMiddlewareAdd(t *testing.T) { } router.Use(banalMw) if len(router.middlewares) != 3 { - t.Fatal("MiddlewareFunc method was not added correctly") + t.Fatal("Middleware function was not added correctly") } } @@ -55,34 +56,37 @@ func TestMiddleware(t *testing.T) { rw := NewRecorder() req := newRequest("GET", "/") - // Test regular middleware call - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } - - // Middleware should not be called for 404 - req = newRequest("GET", "/not/found") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } + t.Run("regular middleware call", func(t *testing.T) { + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + }) - // Middleware should not be called if there is a method mismatch - req = newRequest("POST", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } + t.Run("not called for 404", func(t *testing.T) { + req = newRequest("GET", "/not/found") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + }) - // Add the middleware again as function - router.Use(mw.Middleware) - req = newRequest("GET", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 3 { - t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled) - } + t.Run("not called for method mismatch", func(t *testing.T) { + req = newRequest("POST", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + }) + t.Run("regular call using function middleware", func(t *testing.T) { + router.Use(mw.Middleware) + req = newRequest("GET", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 3 { + t.Fatalf("Expected %d calls, but got only %d", 3, mw.timesCalled) + } + }) } func TestMiddlewareSubrouter(t *testing.T) { @@ -98,42 +102,56 @@ func TestMiddlewareSubrouter(t *testing.T) { rw := NewRecorder() req := newRequest("GET", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 0 { - t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) - } + t.Run("not called for route outside subrouter", func(t *testing.T) { + router.ServeHTTP(rw, req) + if mw.timesCalled != 0 { + t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) + } + }) - req = newRequest("GET", "/sub/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 0 { - t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) - } + t.Run("not called for subrouter root 404", func(t *testing.T) { + req = newRequest("GET", "/sub/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 0 { + t.Fatalf("Expected %d calls, but got only %d", 0, mw.timesCalled) + } + }) - req = newRequest("GET", "/sub/x") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } + t.Run("called once for route inside subrouter", func(t *testing.T) { + req = newRequest("GET", "/sub/x") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + }) - req = newRequest("GET", "/sub/not/found") - router.ServeHTTP(rw, req) - if mw.timesCalled != 1 { - t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) - } + t.Run("not called for 404 inside subrouter", func(t *testing.T) { + req = newRequest("GET", "/sub/not/found") + router.ServeHTTP(rw, req) + if mw.timesCalled != 1 { + t.Fatalf("Expected %d calls, but got only %d", 1, mw.timesCalled) + } + }) - router.useInterface(mw) + t.Run("middleware added to router", func(t *testing.T) { + router.useInterface(mw) - req = newRequest("GET", "/") - router.ServeHTTP(rw, req) - if mw.timesCalled != 2 { - t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled) - } + t.Run("called once for route outside subrouter", func(t *testing.T) { + req = newRequest("GET", "/") + router.ServeHTTP(rw, req) + if mw.timesCalled != 2 { + t.Fatalf("Expected %d calls, but got only %d", 2, mw.timesCalled) + } + }) - req = newRequest("GET", "/sub/x") - router.ServeHTTP(rw, req) - if mw.timesCalled != 4 { - t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled) - } + t.Run("called twice for route inside subrouter", func(t *testing.T) { + req = newRequest("GET", "/sub/x") + router.ServeHTTP(rw, req) + if mw.timesCalled != 4 { + t.Fatalf("Expected %d calls, but got only %d", 4, mw.timesCalled) + } + }) + }) } func TestMiddlewareExecution(t *testing.T) { @@ -145,30 +163,33 @@ func TestMiddlewareExecution(t *testing.T) { w.Write(handlerStr) }) - rw := NewRecorder() - req := newRequest("GET", "/") + t.Run("responds normally without middleware", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/") - // Test handler-only call - router.ServeHTTP(rw, req) + router.ServeHTTP(rw, req) - if !bytes.Equal(rw.Body.Bytes(), handlerStr) { - t.Fatal("Handler response is not what it should be") - } + if !bytes.Equal(rw.Body.Bytes(), handlerStr) { + t.Fatal("Handler response is not what it should be") + } + }) - // Test middleware call - rw = NewRecorder() + t.Run("responds with handler and middleware response", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/") - router.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(mwStr) - h.ServeHTTP(w, r) + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(mwStr) + h.ServeHTTP(w, r) + }) }) - }) - router.ServeHTTP(rw, req) - if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) { - t.Fatal("Middleware + handler response is not what it should be") - } + router.ServeHTTP(rw, req) + if !bytes.Equal(rw.Body.Bytes(), append(mwStr, handlerStr...)) { + t.Fatal("Middleware + handler response is not what it should be") + } + }) } func TestMiddlewareNotFound(t *testing.T) { @@ -187,26 +208,29 @@ func TestMiddlewareNotFound(t *testing.T) { }) // Test not found call with default handler - rw := NewRecorder() - req := newRequest("GET", "/notfound") + t.Run("not called", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/notfound") - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a 404") - } + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a 404") + } + }) - // Test not found call with custom handler - rw = NewRecorder() - req = newRequest("GET", "/notfound") + t.Run("not called with custom not found handler", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/notfound") - router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Write([]byte("Custom 404 handler")) - }) - router.ServeHTTP(rw, req) + router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Custom 404 handler")) + }) + router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a custom 404") - } + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a custom 404") + } + }) } func TestMiddlewareMethodMismatch(t *testing.T) { @@ -225,27 +249,29 @@ func TestMiddlewareMethodMismatch(t *testing.T) { }) }) - // Test method mismatch - rw := NewRecorder() - req := newRequest("POST", "/") + t.Run("not called", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("POST", "/") - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + }) - // Test not found call - rw = NewRecorder() - req = newRequest("POST", "/") + t.Run("not called with custom method not allowed handler", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("POST", "/") - router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Write([]byte("Method not allowed")) - }) - router.ServeHTTP(rw, req) + router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Method not allowed")) + }) + router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + }) } func TestMiddlewareNotFoundSubrouter(t *testing.T) { @@ -269,27 +295,29 @@ func TestMiddlewareNotFoundSubrouter(t *testing.T) { }) }) - // Test not found call for default handler - rw := NewRecorder() - req := newRequest("GET", "/sub/notfound") + t.Run("not called", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/sub/notfound") - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a 404") - } + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a 404") + } + }) - // Test not found call with custom handler - rw = NewRecorder() - req = newRequest("GET", "/sub/notfound") + t.Run("not called with custom not found handler", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/sub/notfound") - subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Write([]byte("Custom 404 handler")) - }) - router.ServeHTTP(rw, req) + subrouter.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Custom 404 handler")) + }) + router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a custom 404") - } + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a custom 404") + } + }) } func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { @@ -313,27 +341,29 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { }) }) - // Test method mismatch without custom handler - rw := NewRecorder() - req := newRequest("POST", "/sub/") + t.Run("not called", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("POST", "/sub/") - router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } + router.ServeHTTP(rw, req) + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + }) - // Test method mismatch with custom handler - rw = NewRecorder() - req = newRequest("POST", "/sub/") + t.Run("not called with custom method not allowed handler", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("POST", "/sub/") - router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Write([]byte("Method not allowed")) - }) - router.ServeHTTP(rw, req) + router.MethodNotAllowedHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("Method not allowed")) + }) + router.ServeHTTP(rw, req) - if bytes.Contains(rw.Body.Bytes(), mwStr) { - t.Fatal("Middleware was called for a method mismatch") - } + if bytes.Contains(rw.Body.Bytes(), mwStr) { + t.Fatal("Middleware was called for a method mismatch") + } + }) } func TestCORSMethodMiddleware(t *testing.T) { @@ -358,21 +388,23 @@ func TestCORSMethodMiddleware(t *testing.T) { router.Use(CORSMethodMiddleware(router)) - for _, tt := range cases { - rr := httptest.NewRecorder() - req := newRequest(tt.method, tt.testURL) + for i, tt := range cases { + t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { + rr := httptest.NewRecorder() + req := newRequest(tt.method, tt.testURL) - router.ServeHTTP(rr, req) + router.ServeHTTP(rr, req) - if rr.Body.String() != tt.response { - t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String()) - } + if rr.Body.String() != tt.response { + t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String()) + } - allowedMethods := rr.Header().Get("Access-Control-Allow-Methods") + allowedMethods := rr.Header().Get("Access-Control-Allow-Methods") - if allowedMethods != tt.expectedAllowedMethods { - t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods) - } + if allowedMethods != tt.expectedAllowedMethods { + t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods) + } + }) } } @@ -411,27 +443,33 @@ func TestMiddlewareOnMultiSubrouter(t *testing.T) { }) }) - rw := NewRecorder() - req := newRequest("GET", "/first") + t.Run("/first uses first middleware", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/first") - router.ServeHTTP(rw, req) - if rw.Body.String() != first { - t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String()) - } + router.ServeHTTP(rw, req) + if rw.Body.String() != first { + t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", first, rw.Body.String()) + } + }) - rw = NewRecorder() - req = newRequest("GET", "/second") + t.Run("/second uses second middleware", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/second") - router.ServeHTTP(rw, req) - if rw.Body.String() != second { - t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String()) - } + router.ServeHTTP(rw, req) + if rw.Body.String() != second { + t.Fatalf("Middleware did not run: expected %s middleware to write a response (got %s)", second, rw.Body.String()) + } + }) - rw = NewRecorder() - req = newRequest("GET", "/second/not-exist") + t.Run("uses not found handler", func(t *testing.T) { + rw := NewRecorder() + req := newRequest("GET", "/second/not-exist") - router.ServeHTTP(rw, req) - if rw.Body.String() != notFound { - t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String()) - } + router.ServeHTTP(rw, req) + if rw.Body.String() != notFound { + t.Fatalf("Notfound handler did not run: expected %s for not-exist, (got %s)", notFound, rw.Body.String()) + } + }) } From d70f7b4baacbd8115844925606cf68ee63d438cc Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 29 Jun 2019 11:01:59 -0700 Subject: [PATCH 63/89] Delete ISSUE_TEMPLATE.md (#492) --- ISSUE_TEMPLATE.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 232be82e..00000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ -**What version of Go are you running?** (Paste the output of `go version`) - - -**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`) - - -**Describe your problem** (and what you have tried so far) - - -**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it) - From 05347690167e152b85ece29fdfa9cc12a7b8c385 Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Sat, 29 Jun 2019 13:52:29 -0700 Subject: [PATCH 64/89] Improve CORS Method Middleware (#477) * More sensical CORSMethodMiddleware * Only sets Access-Control-Allow-Methods on valid preflight requests * Does not return after setting the Access-Control-Allow-Methods header * Does not append OPTIONS header to Access-Control-Allow-Methods regardless of whether there is an OPTIONS method matcher * Adds tests for the listed behavior * Add example for CORSMethodMiddleware * Do not check for preflight and add documentation to the README * Use http.MethodOptions instead of "OPTIONS" * Add link to CORSMethodMiddleware section to readme * Add test for unmatching route methods * Rename CORS Method Middleware to Handling CORS Requests in README * Link CORSMethodMiddleware in README to godoc * Break CORSMethodMiddleware doc into bullets for readability * Add comment about specifying OPTIONS to example in README for CORSMethodMiddleware * Document cURL command used for testing CORS Method Middleware * Update comment in example to "Handle the request" * Add explicit comment about OPTIONS matchers to CORSMethodMiddleware doc * Update circleci config to only check gofmt diff on latest go version * Break up gofmt and go vet checks into separate steps. * Use canonical circleci config --- .circleci/config.yml | 16 +++- README.md | 68 +++++++++++++ example_cors_method_middleware_test.go | 37 +++++++ middleware.go | 61 ++++++------ middleware_test.go | 128 +++++++++++++++++++------ 5 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 example_cors_method_middleware_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index c0fb9de3..d7d96d14 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,8 +11,20 @@ jobs: - checkout - run: go version - run: go get -t -v ./... - - run: diff -u <(echo -n) <(gofmt -d .) - - run: if [[ "$LATEST" = true ]]; then go vet -v .; fi + # Only run gofmt, vet & lint against the latest Go version + - run: > + if [[ "$LATEST" = true ]]; then + go get -u golang.org/x/lint/golint + golint ./... + fi + - run: > + if [[ "$LATEST" = true ]]; then + diff -u <(echo -n) <(gofmt -d .) + fi + - run: > + if [[ "$LATEST" = true ]]; then + go vet -v . + fi - run: go test -v -race ./... "latest": diff --git a/README.md b/README.md index 08f63a10..92e422ee 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) * [Middleware](#middleware) +* [Handling CORS Requests](#handling-cors-requests) * [Testing Handlers](#testing-handlers) * [Full Example](#full-example) @@ -492,6 +493,73 @@ r.Use(amw.Middleware) Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. +### Handling CORS Requests + +[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. + +* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` +* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route +* If you do not specify any methods, then: +> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. + +Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: + +```go +package main + +import ( + "net/http" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + + // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers + r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) + r.Use(mux.CORSMethodMiddleware(r)) + + http.ListenAndServe(":8080", r) +} + +func fooHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + return + } + + w.Write([]byte("foo")) +} +``` + +And an request to `/foo` using something like: + +```bash +curl localhost:8080/foo -v +``` + +Would look like: + +```bash +* Trying ::1... +* TCP_NODELAY set +* Connected to localhost (::1) port 8080 (#0) +> GET /foo HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.59.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS +< Access-Control-Allow-Origin: * +< Date: Fri, 28 Jun 2019 20:13:30 GMT +< Content-Length: 3 +< Content-Type: text/plain; charset=utf-8 +< +* Connection #0 to host localhost left intact +foo +``` + ### Testing Handlers Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. diff --git a/example_cors_method_middleware_test.go b/example_cors_method_middleware_test.go new file mode 100644 index 00000000..00929fce --- /dev/null +++ b/example_cors_method_middleware_test.go @@ -0,0 +1,37 @@ +package mux_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/gorilla/mux" +) + +func ExampleCORSMethodMiddleware() { + r := mux.NewRouter() + + r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { + // Handle the request + }).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "http://example.com") + w.Header().Set("Access-Control-Max-Age", "86400") + }).Methods(http.MethodOptions) + + r.Use(mux.CORSMethodMiddleware(r)) + + rw := httptest.NewRecorder() + req, _ := http.NewRequest("OPTIONS", "/foo", nil) // needs to be OPTIONS + req.Header.Set("Access-Control-Request-Method", "POST") // needs to be non-empty + req.Header.Set("Access-Control-Request-Headers", "Authorization") // needs to be non-empty + req.Header.Set("Origin", "http://example.com") // needs to be non-empty + + r.ServeHTTP(rw, req) + + fmt.Println(rw.Header().Get("Access-Control-Allow-Methods")) + fmt.Println(rw.Header().Get("Access-Control-Allow-Origin")) + // Output: + // GET,PUT,PATCH,OPTIONS + // http://example.com +} diff --git a/middleware.go b/middleware.go index ceb812ce..cf2b26dc 100644 --- a/middleware.go +++ b/middleware.go @@ -32,37 +32,19 @@ func (r *Router) useInterface(mw middleware) { r.middlewares = append(r.middlewares, mw) } -// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header -// on a request, by matching routes based only on paths. It also handles -// OPTIONS requests, by settings Access-Control-Allow-Methods, and then -// returning without calling the next http handler. +// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header +// on requests for routes that have an OPTIONS method matcher to all the method matchers on +// the route. Routes that do not explicitly handle OPTIONS requests will not be processed +// by the middleware. See examples for usage. func CORSMethodMiddleware(r *Router) MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - var allMethods []string - - err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { - for _, m := range route.matchers { - if _, ok := m.(*routeRegexp); ok { - if m.Match(req, &RouteMatch{}) { - methods, err := route.GetMethods() - if err != nil { - return err - } - - allMethods = append(allMethods, methods...) - } - break - } - } - return nil - }) - + allMethods, err := getAllMethodsForRoute(r, req) if err == nil { - w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ",")) - - if req.Method == "OPTIONS" { - return + for _, v := range allMethods { + if v == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) + } } } @@ -70,3 +52,28 @@ func CORSMethodMiddleware(r *Router) MiddlewareFunc { }) } } + +// getAllMethodsForRoute returns all the methods from method matchers matching a given +// request. +func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { + var allMethods []string + + err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { + for _, m := range route.matchers { + if _, ok := m.(*routeRegexp); ok { + if m.Match(req, &RouteMatch{}) { + methods, err := route.GetMethods() + if err != nil { + return err + } + + allMethods = append(allMethods, methods...) + } + break + } + } + return nil + }) + + return allMethods, err +} diff --git a/middleware_test.go b/middleware_test.go index 30df2746..27647afe 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -2,9 +2,7 @@ package mux import ( "bytes" - "fmt" "net/http" - "net/http/httptest" "testing" ) @@ -367,42 +365,114 @@ func TestMiddlewareMethodMismatchSubrouter(t *testing.T) { } func TestCORSMethodMiddleware(t *testing.T) { - router := NewRouter() - - cases := []struct { - path string - response string - method string - testURL string - expectedAllowedMethods string + testCases := []struct { + name string + registerRoutes func(r *Router) + requestHeader http.Header + requestMethod string + requestPath string + expectedAccessControlAllowMethodsHeader string + expectedResponse string }{ - {"/g/{o}", "a", "POST", "/g/asdf", "POST,PUT,GET,OPTIONS"}, - {"/g/{o}", "b", "PUT", "/g/bla", "POST,PUT,GET,OPTIONS"}, - {"/g/{o}", "c", "GET", "/g/orilla", "POST,PUT,GET,OPTIONS"}, - {"/g", "d", "POST", "/g", "POST,OPTIONS"}, + { + name: "does not set without OPTIONS matcher", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + }, + requestMethod: "GET", + requestPath: "/foo", + expectedAccessControlAllowMethodsHeader: "", + expectedResponse: "a", + }, + { + name: "sets on non OPTIONS", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) + }, + requestMethod: "GET", + requestPath: "/foo", + expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", + expectedResponse: "a", + }, + { + name: "sets without preflight headers", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) + }, + requestMethod: "OPTIONS", + requestPath: "/foo", + expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", + expectedResponse: "b", + }, + { + name: "does not set on error", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("a")) + }, + requestMethod: "OPTIONS", + requestPath: "/foo", + expectedAccessControlAllowMethodsHeader: "", + expectedResponse: "a", + }, + { + name: "sets header on valid preflight", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + r.HandleFunc("/foo", stringHandler("b")).Methods(http.MethodOptions) + }, + requestMethod: "OPTIONS", + requestPath: "/foo", + requestHeader: http.Header{ + "Access-Control-Request-Method": []string{"GET"}, + "Access-Control-Request-Headers": []string{"Authorization"}, + "Origin": []string{"http://example.com"}, + }, + expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", + expectedResponse: "b", + }, + { + name: "does not set methods from unmatching routes", + registerRoutes: func(r *Router) { + r.HandleFunc("/foo", stringHandler("c")).Methods(http.MethodDelete) + r.HandleFunc("/foo/bar", stringHandler("a")).Methods(http.MethodGet, http.MethodPut, http.MethodPatch) + r.HandleFunc("/foo/bar", stringHandler("b")).Methods(http.MethodOptions) + }, + requestMethod: "OPTIONS", + requestPath: "/foo/bar", + requestHeader: http.Header{ + "Access-Control-Request-Method": []string{"GET"}, + "Access-Control-Request-Headers": []string{"Authorization"}, + "Origin": []string{"http://example.com"}, + }, + expectedAccessControlAllowMethodsHeader: "GET,PUT,PATCH,OPTIONS", + expectedResponse: "b", + }, } - for _, tt := range cases { - router.HandleFunc(tt.path, stringHandler(tt.response)).Methods(tt.method) - } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + router := NewRouter() - router.Use(CORSMethodMiddleware(router)) + tt.registerRoutes(router) - for i, tt := range cases { - t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { - rr := httptest.NewRecorder() - req := newRequest(tt.method, tt.testURL) + router.Use(CORSMethodMiddleware(router)) - router.ServeHTTP(rr, req) + rw := NewRecorder() + req := newRequest(tt.requestMethod, tt.requestPath) + req.Header = tt.requestHeader - if rr.Body.String() != tt.response { - t.Errorf("Expected body '%s', found '%s'", tt.response, rr.Body.String()) - } + router.ServeHTTP(rw, req) - allowedMethods := rr.Header().Get("Access-Control-Allow-Methods") + actualMethodsHeader := rw.Header().Get("Access-Control-Allow-Methods") + if actualMethodsHeader != tt.expectedAccessControlAllowMethodsHeader { + t.Fatalf("Expected Access-Control-Allow-Methods to equal %s but got %s", tt.expectedAccessControlAllowMethodsHeader, actualMethodsHeader) + } - if allowedMethods != tt.expectedAllowedMethods { - t.Errorf("Expected Access-Control-Allow-Methods '%s', found '%s'", tt.expectedAllowedMethods, allowedMethods) + actualResponse := rw.Body.String() + if actualResponse != tt.expectedResponse { + t.Fatalf("Expected response to equal %s but got %s", tt.expectedResponse, actualResponse) } }) } From 00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 29 Jun 2019 21:17:52 -0700 Subject: [PATCH 65/89] Update stale.yml (#494) --- .github/stale.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index de8a6780..f4b12d30 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,10 +1,10 @@ -daysUntilStale: 60 -daysUntilClose: 7 +daysUntilStale: 75 +daysUntilClose: 14 # Issues with these labels will never be considered stale exemptLabels: - - v2 - - needs-review - - work-required + - proposal + - needs review + - build system staleLabel: stale markComment: > This issue has been automatically marked as stale because it hasn't seen From d83b6ffe499a29cc05fc977988d0392851779620 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Mon, 1 Jul 2019 13:26:33 -0700 Subject: [PATCH 66/89] Update config.yml (#495) * Update config.yml * Update config.yml --- .circleci/config.yml | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d7d96d14..536bc119 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,23 +8,35 @@ jobs: - image: circleci/golang:latest working_directory: /go/src/github.com/gorilla/mux steps: &steps + # Our build steps: we checkout the repo, fetch our deps, lint, and finally + # run "go test" on the package. - checkout + # Logs the version in our build logs, for posterity - run: go version - - run: go get -t -v ./... + - run: + name: "Fetch dependencies" + command: > + go get -t -v ./... # Only run gofmt, vet & lint against the latest Go version - - run: > - if [[ "$LATEST" = true ]]; then - go get -u golang.org/x/lint/golint - golint ./... - fi - - run: > - if [[ "$LATEST" = true ]]; then - diff -u <(echo -n) <(gofmt -d .) - fi - - run: > - if [[ "$LATEST" = true ]]; then - go vet -v . - fi + - run: + name: "Run golint" + command: > + if [ "${LATEST}" = true ] && [ -z "${SKIP_GOLINT}" ]; then + go get -u golang.org/x/lint/golint + golint ./... + fi + - run: + name: "Run gofmt" + command: > + if [[ "${LATEST}" = true ]]; then + diff -u <(echo -n) <(gofmt -d -e .) + fi + - run: + name: "Run go vet" + command: > + if [[ "${LATEST}" = true ]]; then + go vet -v ./... + fi - run: go test -v -race ./... "latest": From 50fbc3e7fbfcdb4fb850686588071e5f0bdd4a0a Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 20 Jul 2019 16:48:32 +0200 Subject: [PATCH 67/89] Avoid unnecessary conversion (#502) No need to convert here. --- mux_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index f5c1e9c5..34c00dd2 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1943,7 +1943,7 @@ type TestA301ResponseWriter struct { } func (ho *TestA301ResponseWriter) Header() http.Header { - return http.Header(ho.hh) + return ho.hh } func (ho *TestA301ResponseWriter) Write(b []byte) (int, error) { From eab9c4f3d22d907d728aa0f5918934357866249e Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 20 Jul 2019 16:49:38 +0200 Subject: [PATCH 68/89] Simplify code (#501) Use a single append call instead of a ranged for loop. --- mux.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mux.go b/mux.go index a2cd193e..26f9582a 100644 --- a/mux.go +++ b/mux.go @@ -111,10 +111,8 @@ func copyRouteConf(r routeConf) routeConf { c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) } - c.matchers = make([]matcher, 0, len(r.matchers)) - for _, m := range r.matchers { - c.matchers = append(c.matchers, m) - } + c.matchers = make([]matcher, len(r.matchers)) + copy(c.matchers, r.matchers) return c } From 7a1bf406d6f5f8cfef4802eb0d949606c6ee7aea Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Sat, 20 Jul 2019 07:53:35 -0700 Subject: [PATCH 69/89] [docs] Add documentation for using mux to serve a SPA (#493) * Add documentation for using mux to serve a SPA * r -> router to prevent shadowing * Expand SPA acronym * BrowserRouter link * Add more comments to explain how the spaHandler.ServeHTTP method works --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ 2 files changed, 90 insertions(+) diff --git a/README.md b/README.md index 92e422ee..9c5b86a5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Examples](#examples) * [Matching Routes](#matching-routes) * [Static Files](#static-files) +* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) @@ -212,6 +213,93 @@ func main() { } ``` +### Serving Single Page Applications + +Most of the time it makes sense to serve your SPA on a separate web server from your API, +but sometimes it's desirable to serve them both from one place. It's possible to write a simple +handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage +mux's powerful routing for your API endpoints. + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) + if err != nil { + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // prepend the path with the path to the static directory + path = filepath.Join(h.staticPath, path) + + // check whether a file exists at the given path + _, err = os.Stat(path) + if os.IsNotExist(err) { + // file does not exist, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + // an example API handler + json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + }) + + spa := spaHandler{staticPath: "build", indexPath: "index.html"} + router.PathPrefix("/").Handler(spa) + + srv := &http.Server{ + Handler: router, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + ### Registered URLs Now let's see how to build registered URLs. diff --git a/go.mod b/go.mod index cfc8ede5..df170a39 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/gorilla/mux + +go 1.12 From e67b3c02c7195c052acff13261f0c9fd1ba53011 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 20 Jul 2019 13:14:35 -0700 Subject: [PATCH 70/89] Remove TravisCI badge (#503) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9c5b86a5..28df9a9f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # gorilla/mux [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) [![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) From e0cdff45b7de56d3ad24ef9871c14fe4238475aa Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 23 Aug 2019 17:44:47 -0700 Subject: [PATCH 71/89] Update README (self-host logo) (#513) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28df9a9f..35eea9f1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) -![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) +![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) https://www.gorillatoolkit.org/pkg/mux From 9536e4053d763b54d935f1ce731a315cfb42b979 Mon Sep 17 00:00:00 2001 From: Jonas De Beukelaer Date: Mon, 26 Aug 2019 01:11:59 +0100 Subject: [PATCH 72/89] bugfix: subrouter custom methodNotAllowed handler returning 404 (#509) (#510) --- mux_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ route.go | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 34c00dd2..e252d391 100644 --- a/mux_test.go +++ b/mux_test.go @@ -9,6 +9,7 @@ import ( "bytes" "errors" "fmt" + "io/ioutil" "net/http" "net/url" "reflect" @@ -2729,6 +2730,63 @@ func TestMethodNotAllowed(t *testing.T) { } } +type customMethodNotAllowedHandler struct { + msg string +} + +func (h customMethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprint(w, h.msg) +} + +func TestSubrouterCustomMethodNotAllowed(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + + router := NewRouter() + router.HandleFunc("/test", handler).Methods(http.MethodGet) + router.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom router handler"} + + subrouter := router.PathPrefix("/sub").Subrouter() + subrouter.HandleFunc("/test", handler).Methods(http.MethodGet) + subrouter.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom sub router handler"} + + testCases := map[string]struct { + path string + expMsg string + }{ + "router method not allowed": { + path: "/test", + expMsg: "custom router handler", + }, + "subrouter method not allowed": { + path: "/sub/test", + expMsg: "custom sub router handler", + }, + } + + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + w := NewRecorder() + req := newRequest(http.MethodPut, tc.path) + + router.ServeHTTP(w, req) + + if w.Code != http.StatusMethodNotAllowed { + tt.Errorf("Expected status code 405 (got %d)", w.Code) + } + + b, err := ioutil.ReadAll(w.Body) + if err != nil { + tt.Errorf("failed to read body: %v", err) + } + + if string(b) != tc.expMsg { + tt.Errorf("expected msg %q, got %q", tc.expMsg, string(b)) + } + }) + } +} + func TestSubrouterNotFound(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } router := NewRouter() diff --git a/route.go b/route.go index 8479c68c..7343d78a 100644 --- a/route.go +++ b/route.go @@ -74,7 +74,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { return false } - if match.MatchErr == ErrMethodMismatch { + if match.MatchErr == ErrMethodMismatch && r.handler != nil { // We found a route which matches request method, clear MatchErr match.MatchErr = nil // Then override the mis-matched handler From e1863a64f3c620ef604fb7905a0351e69f6a51e4 Mon Sep 17 00:00:00 2001 From: Vivek V Date: Tue, 27 Aug 2019 18:28:13 +0530 Subject: [PATCH 73/89] Modified http status codes, using constants provided by the http package (#514) --- mux_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mux_test.go b/mux_test.go index e252d391..edcee572 100644 --- a/mux_test.go +++ b/mux_test.go @@ -2046,7 +2046,7 @@ func TestNoMatchMethodErrorHandler(t *testing.T) { resp := NewRecorder() r.ServeHTTP(resp, req) - if resp.Code != 405 { + if resp.Code != http.StatusMethodNotAllowed { t.Errorf("Expecting code %v", 405) } @@ -2725,7 +2725,7 @@ func TestMethodNotAllowed(t *testing.T) { router.ServeHTTP(w, req) - if w.Code != 405 { + if w.Code != http.StatusMethodNotAllowed { t.Fatalf("Expected status code 405 (got %d)", w.Code) } } @@ -2798,7 +2798,7 @@ func TestSubrouterNotFound(t *testing.T) { router.ServeHTTP(w, req) - if w.Code != 404 { + if w.Code != http.StatusNotFound { t.Fatalf("Expected status code 404 (got %d)", w.Code) } } From 884b5ffcbd3a11b730f0b75f5c86ac408753c34d Mon Sep 17 00:00:00 2001 From: Vivek V Date: Fri, 30 Aug 2019 17:41:56 +0530 Subject: [PATCH 74/89] Added capacity to slice creation, when capacity is known (#516) --- regexp.go | 2 +- route.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/regexp.go b/regexp.go index ac1abcd4..0293a928 100644 --- a/regexp.go +++ b/regexp.go @@ -195,7 +195,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { // url builds a URL part using the given values. func (r *routeRegexp) url(values map[string]string) (string, error) { - urlValues := make([]interface{}, len(r.varsN)) + urlValues := make([]interface{}, len(r.varsN), len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] if !ok { diff --git a/route.go b/route.go index 7343d78a..4098cb79 100644 --- a/route.go +++ b/route.go @@ -635,7 +635,7 @@ func (r *Route) GetQueriesRegexp() ([]string, error) { if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.regexp.String()) } @@ -654,7 +654,7 @@ func (r *Route) GetQueriesTemplates() ([]string, error) { if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.template) } From ff4e71f144166b1dfe3017a146f8ed32a82e688b Mon Sep 17 00:00:00 2001 From: Euan Kemp Date: Thu, 17 Oct 2019 17:48:19 -0700 Subject: [PATCH 75/89] Guess the scheme if r.URL.Scheme is unset (#474) * Guess the scheme if r.URL.Scheme is unset It's not expected that the request's URL is fully populated when used on the server-side (it's more of a client-side field), so we shouldn't expect it to be present. In practice, it's only rarely set at all on the server, making mux's `Schemes` matcher tricky to use as it is. This commit adds a test which would have failed before demonstrating the problem, as well as a fix which I think makes `.Schemes` match what users expect. * [doc] Add more detail to Schemes and URL godocs * Add route url test for schemes * Make httpserver test use more specific scheme matchers * Update test to have different responses per route --- mux_httpserver_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++ mux_test.go | 6 ++---- old_test.go | 17 +++++---------- route.go | 32 ++++++++++++++++++++++++--- 4 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 mux_httpserver_test.go diff --git a/mux_httpserver_test.go b/mux_httpserver_test.go new file mode 100644 index 00000000..5d2f4d3a --- /dev/null +++ b/mux_httpserver_test.go @@ -0,0 +1,49 @@ +// +build go1.9 + +package mux + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSchemeMatchers(t *testing.T) { + router := NewRouter() + router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("hello http world")) + }).Schemes("http") + router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte("hello https world")) + }).Schemes("https") + + assertResponseBody := func(t *testing.T, s *httptest.Server, expectedBody string) { + resp, err := s.Client().Get(s.URL) + if err != nil { + t.Fatalf("unexpected error getting from server: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("expected a status code of 200, got %v", resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error reading body: %v", err) + } + if !bytes.Equal(body, []byte(expectedBody)) { + t.Fatalf("response should be hello world, was: %q", string(body)) + } + } + + t.Run("httpServer", func(t *testing.T) { + s := httptest.NewServer(router) + defer s.Close() + assertResponseBody(t, s, "hello http world") + }) + t.Run("httpsServer", func(t *testing.T) { + s := httptest.NewTLSServer(router) + defer s.Close() + assertResponseBody(t, s, "hello https world") + }) +} diff --git a/mux_test.go b/mux_test.go index edcee572..9a740bb8 100644 --- a/mux_test.go +++ b/mux_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/http/httptest" "net/url" "reflect" "strings" @@ -2895,10 +2896,7 @@ func newRequestWithHeaders(method, url string, headers ...string) *http.Request // newRequestHost a new request with a method, url, and host header func newRequestHost(method, url, host string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) - } + req := httptest.NewRequest(method, url, nil) req.Host = host return req } diff --git a/old_test.go b/old_test.go index b228983c..f088a951 100644 --- a/old_test.go +++ b/old_test.go @@ -385,6 +385,11 @@ var urlBuildingTests = []urlBuildingTest{ vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, url: "http://foo.domain.com/articles/technology/42", }, + { + route: new(Route).Host("example.com").Schemes("https", "http"), + vars: []string{}, + url: "https://example.com", + }, } func TestHeaderMatcher(t *testing.T) { @@ -502,18 +507,6 @@ func TestUrlBuilding(t *testing.T) { url := u.String() if url != v.url { t.Errorf("expected %v, got %v", v.url, url) - /* - reversePath := "" - reverseHost := "" - if v.route.pathTemplate != nil { - reversePath = v.route.pathTemplate.Reverse - } - if v.route.hostTemplate != nil { - reverseHost = v.route.hostTemplate.Reverse - } - - t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) - */ } } diff --git a/route.go b/route.go index 4098cb79..750afe57 100644 --- a/route.go +++ b/route.go @@ -412,11 +412,30 @@ func (r *Route) Queries(pairs ...string) *Route { type schemeMatcher []string func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { - return matchInArray(m, r.URL.Scheme) + scheme := r.URL.Scheme + // https://golang.org/pkg/net/http/#Request + // "For [most] server requests, fields other than Path and RawQuery will be + // empty." + // Since we're an http muxer, the scheme is either going to be http or https + // though, so we can just set it based on the tls termination state. + if scheme == "" { + if r.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + } + return matchInArray(m, scheme) } // Schemes adds a matcher for URL schemes. // It accepts a sequence of schemes to be matched, e.g.: "http", "https". +// If the request's URL has a scheme set, it will be matched against. +// Generally, the URL scheme will only be set if a previous handler set it, +// such as the ProxyHeaders handler from gorilla/handlers. +// If unset, the scheme will be determined based on the request's TLS +// termination state. +// The first argument to Schemes will be used when constructing a route URL. func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) @@ -493,8 +512,8 @@ func (r *Route) Subrouter() *Router { // This also works for host variables: // // r := mux.NewRouter() -// r.Host("{subdomain}.domain.com"). -// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). // Name("article") // // // url.String() will be "http://news.domain.com/articles/technology/42" @@ -502,6 +521,13 @@ func (r *Route) Subrouter() *Router { // "category", "technology", // "id", "42") // +// The scheme of the resulting url will be the first argument that was passed to Schemes: +// +// // url.String() will be "https://example.com" +// r := mux.NewRouter() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() +// // All variables defined in the route are required, and their values must // conform to the corresponding patterns. func (r *Route) URL(pairs ...string) (*url.URL, error) { From f395758b854c4efa789b8c1e9b73479704c548cb Mon Sep 17 00:00:00 2001 From: Franklin Harding <32021905+fharding1@users.noreply.github.com> Date: Thu, 24 Oct 2019 05:12:56 -0700 Subject: [PATCH 76/89] Remove/cleanup request context helpers (#525) * Remove context helpers in context.go * Update request context funcs to take concrete types * Move TestNativeContextMiddleware to mux_test.go * Clarify KeepContext Go 1.7+ comment Mux doesn't build on Go < 1.7 so the comment doesn't really need to clarify anymore. --- context.go | 18 ------------------ context_test.go | 30 ------------------------------ mux.go | 22 ++++++++++++---------- mux_test.go | 24 ++++++++++++++++++++++++ test_helpers.go | 2 +- 5 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 context.go delete mode 100644 context_test.go diff --git a/context.go b/context.go deleted file mode 100644 index 665940a2..00000000 --- a/context.go +++ /dev/null @@ -1,18 +0,0 @@ -package mux - -import ( - "context" - "net/http" -) - -func contextGet(r *http.Request, key interface{}) interface{} { - return r.Context().Value(key) -} - -func contextSet(r *http.Request, key, val interface{}) *http.Request { - if val == nil { - return r - } - - return r.WithContext(context.WithValue(r.Context(), key, val)) -} diff --git a/context_test.go b/context_test.go deleted file mode 100644 index d8a56b42..00000000 --- a/context_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package mux - -import ( - "context" - "net/http" - "testing" - "time" -) - -func TestNativeContextMiddleware(t *testing.T) { - withTimeout := func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), time.Minute) - defer cancel() - h.ServeHTTP(w, r.WithContext(ctx)) - }) - } - - r := NewRouter() - r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := Vars(r) - if vars["foo"] != "bar" { - t.Fatal("Expected foo var to be set") - } - }))) - - rec := NewRecorder() - req := newRequest("GET", "/path/bar") - r.ServeHTTP(rec, req) -} diff --git a/mux.go b/mux.go index 26f9582a..c9ba6470 100644 --- a/mux.go +++ b/mux.go @@ -5,6 +5,7 @@ package mux import ( + "context" "errors" "fmt" "net/http" @@ -58,8 +59,7 @@ type Router struct { // If true, do not clear the request context after handling the request. // - // Deprecated: No effect when go1.7+ is used, since the context is stored - // on the request itself. + // Deprecated: No effect, since the context is stored on the request itself. KeepContext bool // Slice of middlewares to be called after a match is found @@ -195,8 +195,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { var handler http.Handler if r.Match(req, &match) { handler = match.Handler - req = setVars(req, match.Vars) - req = setCurrentRoute(req, match.Route) + req = requestWithVars(req, match.Vars) + req = requestWithRoute(req, match.Route) } if handler == nil && match.MatchErr == ErrMethodMismatch { @@ -426,7 +426,7 @@ const ( // Vars returns the route variables for the current request, if any. func Vars(r *http.Request) map[string]string { - if rv := contextGet(r, varsKey); rv != nil { + if rv := r.Context().Value(varsKey); rv != nil { return rv.(map[string]string) } return nil @@ -438,18 +438,20 @@ func Vars(r *http.Request) map[string]string { // after the handler returns, unless the KeepContext option is set on the // Router. func CurrentRoute(r *http.Request) *Route { - if rv := contextGet(r, routeKey); rv != nil { + if rv := r.Context().Value(routeKey); rv != nil { return rv.(*Route) } return nil } -func setVars(r *http.Request, val interface{}) *http.Request { - return contextSet(r, varsKey, val) +func requestWithVars(r *http.Request, vars map[string]string) *http.Request { + ctx := context.WithValue(r.Context(), varsKey, vars) + return r.WithContext(ctx) } -func setCurrentRoute(r *http.Request, val interface{}) *http.Request { - return contextSet(r, routeKey, val) +func requestWithRoute(r *http.Request, route *Route) *http.Request { + ctx := context.WithValue(r.Context(), routeKey, route) + return r.WithContext(ctx) } // ---------------------------------------------------------------------------- diff --git a/mux_test.go b/mux_test.go index 9a740bb8..1c906689 100644 --- a/mux_test.go +++ b/mux_test.go @@ -7,6 +7,7 @@ package mux import ( "bufio" "bytes" + "context" "errors" "fmt" "io/ioutil" @@ -16,6 +17,7 @@ import ( "reflect" "strings" "testing" + "time" ) func (r *Route) GoString() string { @@ -2804,6 +2806,28 @@ func TestSubrouterNotFound(t *testing.T) { } } +func TestContextMiddleware(t *testing.T) { + withTimeout := func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + h.ServeHTTP(w, r.WithContext(ctx)) + }) + } + + r := NewRouter() + r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := Vars(r) + if vars["foo"] != "bar" { + t.Fatal("Expected foo var to be set") + } + }))) + + rec := NewRecorder() + req := newRequest("GET", "/path/bar") + r.ServeHTTP(rec, req) +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int diff --git a/test_helpers.go b/test_helpers.go index 32ecffde..5f5c496d 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -15,5 +15,5 @@ import "net/http" // can be set by making a route that captures the required variables, // starting a server and sending the request to that server. func SetURLVars(r *http.Request, val map[string]string) *http.Request { - return setVars(r, val) + return requestWithVars(r, val) } From 946b6237eb8d0ce3225f502b7fd4208d0b60ce5f Mon Sep 17 00:00:00 2001 From: Franklin Harding Date: Thu, 14 Nov 2019 12:19:09 -0800 Subject: [PATCH 77/89] Fix the CORSMethodMiddleware bug with subrouters * Adds a test case for the repro given in issue #534 * Fixes the logic in CORSMethodMiddleware to handle matching routes better --- middleware.go | 25 ++++++++++--------------- middleware_test.go | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/middleware.go b/middleware.go index cf2b26dc..cb51c565 100644 --- a/middleware.go +++ b/middleware.go @@ -58,22 +58,17 @@ func CORSMethodMiddleware(r *Router) MiddlewareFunc { func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { var allMethods []string - err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { - for _, m := range route.matchers { - if _, ok := m.(*routeRegexp); ok { - if m.Match(req, &RouteMatch{}) { - methods, err := route.GetMethods() - if err != nil { - return err - } - - allMethods = append(allMethods, methods...) - } - break + for _, route := range r.routes { + var match RouteMatch + if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { + methods, err := route.GetMethods() + if err != nil { + return nil, err } + + allMethods = append(allMethods, methods...) } - return nil - }) + } - return allMethods, err + return allMethods, nil } diff --git a/middleware_test.go b/middleware_test.go index 27647afe..e9f0ef55 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -478,6 +478,26 @@ func TestCORSMethodMiddleware(t *testing.T) { } } +func TestCORSMethodMiddlewareSubrouter(t *testing.T) { + router := NewRouter().StrictSlash(true) + + subrouter := router.PathPrefix("/test").Subrouter() + subrouter.HandleFunc("/hello", stringHandler("a")).Methods(http.MethodGet, http.MethodOptions, http.MethodPost) + subrouter.HandleFunc("/hello/{name}", stringHandler("b")).Methods(http.MethodGet, http.MethodOptions) + + subrouter.Use(CORSMethodMiddleware(subrouter)) + + rw := NewRecorder() + req := newRequest("GET", "/test/hello/asdf") + router.ServeHTTP(rw, req) + + actualMethods := rw.Header().Get("Access-Control-Allow-Methods") + expectedMethods := "GET,OPTIONS" + if actualMethods != expectedMethods { + t.Fatalf("expected methods %q but got: %q", expectedMethods, actualMethods) + } +} + func TestMiddlewareOnMultiSubrouter(t *testing.T) { first := "first" second := "second" From 4de8a5a4d283677c69afa1a86a044c8451633a18 Mon Sep 17 00:00:00 2001 From: wangming Date: Tue, 19 Nov 2019 21:02:14 +0800 Subject: [PATCH 78/89] fix headers regexp test (#536) --- mux_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mux_test.go b/mux_test.go index 1c906689..2d8d2b3e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -684,8 +684,8 @@ func TestHeaders(t *testing.T) { }, { title: "Headers route, regex header values to match", - route: new(Route).Headers("foo", "ba[zr]"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar"}), + route: new(Route).HeadersRegexp("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baw"}), vars: map[string]string{}, host: "", path: "", From 49c01487a141b49f8ffe06277f3dca3ee80a55fa Mon Sep 17 00:00:00 2001 From: Veselkov Konstantin Date: Thu, 21 Nov 2019 21:05:00 +0400 Subject: [PATCH 79/89] lint: Remove golint warning (#526) --- regexp.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/regexp.go b/regexp.go index 0293a928..b5a15ed9 100644 --- a/regexp.go +++ b/regexp.go @@ -181,16 +181,16 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { } } return r.regexp.MatchString(host) - } else { - if r.regexpType == regexpTypeQuery { - return r.matchQueryString(req) - } - path := req.URL.Path - if r.options.useEncodedPath { - path = req.URL.EscapedPath() - } - return r.regexp.MatchString(path) } + + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) } // url builds a URL part using the given values. From 75dcda0896e109a2a22c9315bca3bb21b87b2ba5 Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Sun, 12 Jan 2020 20:17:43 +0100 Subject: [PATCH 80/89] perf: reduce allocations in (*routeRegexp).getURLQuery (#544) A production server is seeing a significant amount of allocations in (*routeRegexp).getURLQuery Since it is only interested in a single value and only the first value we create a specialized function for that. Comparing a few parameter parsing scenarios: ``` Benchmark_findQueryKey/0-8 7184014 168 ns/op 0 B/op 0 allocs/op Benchmark_findQueryKey/1-8 5307873 227 ns/op 48 B/op 3 allocs/op Benchmark_findQueryKey/2-8 1560836 770 ns/op 483 B/op 10 allocs/op Benchmark_findQueryKey/3-8 1296200 931 ns/op 559 B/op 11 allocs/op Benchmark_findQueryKey/4-8 666502 1769 ns/op 3 B/op 1 allocs/op Benchmark_findQueryKeyGoLib/0-8 1740973 690 ns/op 864 B/op 8 allocs/op Benchmark_findQueryKeyGoLib/1-8 3029618 393 ns/op 432 B/op 4 allocs/op Benchmark_findQueryKeyGoLib/2-8 461427 2511 ns/op 1542 B/op 24 allocs/op Benchmark_findQueryKeyGoLib/3-8 324252 3804 ns/op 1984 B/op 28 allocs/op Benchmark_findQueryKeyGoLib/4-8 69348 14928 ns/op 12716 B/op 130 allocs/op ``` --- regexp.go | 45 ++++++++++++++++++++++--- regexp_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 regexp_test.go diff --git a/regexp.go b/regexp.go index b5a15ed9..96dd94ad 100644 --- a/regexp.go +++ b/regexp.go @@ -230,14 +230,51 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string { return "" } templateKey := strings.SplitN(r.template, "=", 2)[0] - for key, vals := range req.URL.Query() { - if key == templateKey && len(vals) > 0 { - return key + "=" + vals[0] - } + val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) + if ok { + return templateKey + "=" + val } return "" } +// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. +// If key was not found, empty string and false is returned. +func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { + query := []byte(rawQuery) + for len(query) > 0 { + foundKey := query + if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { + foundKey, query = foundKey[:i], foundKey[i+1:] + } else { + query = query[:0] + } + if len(foundKey) == 0 { + continue + } + var value []byte + if i := bytes.IndexByte(foundKey, '='); i >= 0 { + foundKey, value = foundKey[:i], foundKey[i+1:] + } + if len(foundKey) < len(key) { + // Cannot possibly be key. + continue + } + keyString, err := url.QueryUnescape(string(foundKey)) + if err != nil { + continue + } + if keyString != key { + continue + } + valueString, err := url.QueryUnescape(string(value)) + if err != nil { + continue + } + return valueString, true + } + return "", false +} + func (r *routeRegexp) matchQueryString(req *http.Request) bool { return r.regexp.MatchString(r.getURLQuery(req)) } diff --git a/regexp_test.go b/regexp_test.go new file mode 100644 index 00000000..0d80e6a5 --- /dev/null +++ b/regexp_test.go @@ -0,0 +1,91 @@ +package mux + +import ( + "net/url" + "reflect" + "strconv" + "testing" +) + +func Test_findFirstQueryKey(t *testing.T) { + tests := []string{ + "a=1&b=2", + "a=1&a=2&a=banana", + "ascii=%3Ckey%3A+0x90%3E", + "a=1;b=2", + "a=1&a=2;a=banana", + "a==", + "a=%2", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=1& ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;&a=5", + "a=xxxxxxxxxxxxxxxx&b=YYYYYYYYYYYYYYY&c=ppppppppppppppppppp&f=ttttttttttttttttt&a=uuuuuuuuuuuuu", + } + for _, query := range tests { + t.Run(query, func(t *testing.T) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + for key, want := range all { + t.Run(key, func(t *testing.T) { + got, ok := findFirstQueryKey(query, key) + if !ok { + t.Error("Did not get expected key", key) + } + if !reflect.DeepEqual(got, want[0]) { + t.Errorf("findFirstQueryKey(%s,%s) = %v, want %v", query, key, got, want[0]) + } + }) + } + }) + } +} + +func Benchmark_findQueryKey(b *testing.B) { + tests := []string{ + "a=1&b=2", + "ascii=%3Ckey%3A+0x90%3E", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", + "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", + } + for i, query := range tests { + b.Run(strconv.Itoa(i), func(b *testing.B) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for key, _ := range all { + _, _ = findFirstQueryKey(query, key) + } + } + }) + } +} + +func Benchmark_findQueryKeyGoLib(b *testing.B) { + tests := []string{ + "a=1&b=2", + "ascii=%3Ckey%3A+0x90%3E", + "a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30", + "a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu", + "a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=", + } + for i, query := range tests { + b.Run(strconv.Itoa(i), func(b *testing.B) { + // Check against url.ParseQuery, ignoring the error. + all, _ := url.ParseQuery(query) + var u url.URL + u.RawQuery = query + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for key, _ := range all { + v := u.Query()[key] + if len(v) > 0 { + _ = v[0] + } + } + } + }) + } +} From 948bec34b5168796bf3fc4dbb09215baa970351a Mon Sep 17 00:00:00 2001 From: Eric Skoglund Date: Sun, 17 May 2020 06:02:54 +0200 Subject: [PATCH 81/89] docs: Remove stale text from comment. (#568) Comment for CurrentRoute claimed that setting the KeepContext option would propagate the Route even after the request. The KeepContext option is deprecated and has no effect. --- mux.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mux.go b/mux.go index c9ba6470..782a34b2 100644 --- a/mux.go +++ b/mux.go @@ -435,8 +435,7 @@ func Vars(r *http.Request) map[string]string { // CurrentRoute returns the matched route for the current request, if any. // This only works when called inside the handler of the matched route // because the matched route is stored in the request context which is cleared -// after the handler returns, unless the KeepContext option is set on the -// Router. +// after the handler returns. func CurrentRoute(r *http.Request) *Route { if rv := r.Context().Value(routeKey); rv != nil { return rv.(*Route) From 98cb6bf42e086f6af920b965c38cacc07402d51b Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Sat, 11 Jul 2020 13:05:21 -0700 Subject: [PATCH 82/89] fix: regression in vars extract for wildcard host (#579) Continuing from PR #447 we have to add extra check to ignore the port as well add tests to cover this case --- old_test.go | 23 ++++++++++++++++++++++- regexp.go | 6 ++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/old_test.go b/old_test.go index f088a951..96dbe337 100644 --- a/old_test.go +++ b/old_test.go @@ -260,6 +260,18 @@ var hostMatcherTests = []hostMatcherTest{ vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, result: true, }, + { + matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}:{port:.*}"), + url: "http://abc.def.ghi:65535/", + vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi", "port": "65535"}, + result: true, + }, + { + matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), + url: "http://abc.def.ghi:65535/", + vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, + result: true, + }, { matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), url: "http://a.b.c/", @@ -365,6 +377,11 @@ var urlBuildingTests = []urlBuildingTest{ vars: []string{"subdomain", "bar"}, url: "http://bar.domain.com", }, + { + route: new(Route).Host("{subdomain}.domain.com:{port:.*}"), + vars: []string{"subdomain", "bar", "port", "65535"}, + url: "http://bar.domain.com:65535", + }, { route: new(Route).Host("foo.domain.com").Path("/articles"), vars: []string{}, @@ -412,7 +429,11 @@ func TestHeaderMatcher(t *testing.T) { func TestHostMatcher(t *testing.T) { for _, v := range hostMatcherTests { - request, _ := http.NewRequest("GET", v.url, nil) + request, err := http.NewRequest("GET", v.url, nil) + if err != nil { + t.Errorf("http.NewRequest failed %#v", err) + continue + } var routeMatch RouteMatch result := v.matcher.Match(request, &routeMatch) vars := routeMatch.Vars diff --git a/regexp.go b/regexp.go index 96dd94ad..0144842b 100644 --- a/regexp.go +++ b/regexp.go @@ -325,6 +325,12 @@ func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { // Store host variables. if v.host != nil { host := getHost(req) + if v.host.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } matches := v.host.regexp.FindStringSubmatchIndex(host) if len(matches) > 0 { extractVars(host, matches, v.host.varsN, m.Vars) From d07530f46e1eec4e40346e24af34dcc6750ad39f Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 12 Sep 2020 12:20:56 -0700 Subject: [PATCH 83/89] build: CircleCI 2.1 + build matrix (#595) * build: CircleCI 2.1 + build matrix * build: drop Go 1.9 & Go 1.10 * build: remove erroneous version --- .circleci/config.yml | 103 ++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 536bc119..ead3e1d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,87 +1,70 @@ -version: 2.0 +version: 2.1 jobs: - # Base test configuration for Go library tests Each distinct version should - # inherit this base, and override (at least) the container image used. - "test": &test + "test": + parameters: + version: + type: string + default: "latest" + golint: + type: boolean + default: true + modules: + type: boolean + default: true + goproxy: + type: string + default: "" docker: - - image: circleci/golang:latest + - image: "circleci/golang:<< parameters.version >>" working_directory: /go/src/github.com/gorilla/mux - steps: &steps - # Our build steps: we checkout the repo, fetch our deps, lint, and finally - # run "go test" on the package. + environment: + GO111MODULE: "on" + GOPROXY: "<< parameters.goproxy >>" + steps: - checkout - # Logs the version in our build logs, for posterity - - run: go version + - run: + name: "Print the Go version" + command: > + go version - run: name: "Fetch dependencies" command: > - go get -t -v ./... + if [[ << parameters.modules >> = true ]]; then + go mod download + export GO111MODULE=on + else + go get -v ./... + fi # Only run gofmt, vet & lint against the latest Go version - run: name: "Run golint" command: > - if [ "${LATEST}" = true ] && [ -z "${SKIP_GOLINT}" ]; then + if [ << parameters.version >> = "latest" ] && [ << parameters.golint >> = true ]; then go get -u golang.org/x/lint/golint golint ./... fi - run: name: "Run gofmt" command: > - if [[ "${LATEST}" = true ]]; then + if [[ << parameters.version >> = "latest" ]]; then diff -u <(echo -n) <(gofmt -d -e .) fi - run: name: "Run go vet" - command: > - if [[ "${LATEST}" = true ]]; then + command: > + if [[ << parameters.version >> = "latest" ]]; then go vet -v ./... fi - - run: go test -v -race ./... - - "latest": - <<: *test - environment: - LATEST: true - - "1.12": - <<: *test - docker: - - image: circleci/golang:1.12 - - "1.11": - <<: *test - docker: - - image: circleci/golang:1.11 - - "1.10": - <<: *test - docker: - - image: circleci/golang:1.10 - - "1.9": - <<: *test - docker: - - image: circleci/golang:1.9 - - "1.8": - <<: *test - docker: - - image: circleci/golang:1.8 - - "1.7": - <<: *test - docker: - - image: circleci/golang:1.7 + - run: + name: "Run go test (+ race detector)" + command: > + go test -v -race ./... workflows: - version: 2 - build: + tests: jobs: - - "latest" - - "1.12" - - "1.11" - - "1.10" - - "1.9" - - "1.8" - - "1.7" + - test: + matrix: + parameters: + version: ["latest", "1.15", "1.14", "1.13", "1.12", "1.11"] From 3cf0d013e53d62a96c096366d300c84489c26dd5 Mon Sep 17 00:00:00 2001 From: Jille Timmermans Date: Tue, 14 Sep 2021 14:12:19 +0200 Subject: [PATCH 84/89] Include "404" and "405" in the docs (#602) --- mux.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mux.go b/mux.go index 782a34b2..f126a602 100644 --- a/mux.go +++ b/mux.go @@ -46,9 +46,11 @@ func NewRouter() *Router { // This will send all incoming requests to the router. type Router struct { // Configurable Handler to be used when no route matches. + // This can be used to render your own 404 Not Found errors. NotFoundHandler http.Handler // Configurable Handler to be used when the request method does not match the route. + // This can be used to render your own 405 Method Not Allowed errors. MethodNotAllowedHandler http.Handler // Routes to be matched, in order. From 91708ff8e35bafc8612f690a25f5dd0be6f16864 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sun, 12 Dec 2021 08:29:04 -0800 Subject: [PATCH 85/89] docs: update README w.r.t new maintainer ask (#660) * Update README.md * build: gofmt on latest --- README.md | 6 +++++- mux_httpserver_test.go | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35eea9f1..c7e6872b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ ![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) -https://www.gorillatoolkit.org/pkg/mux +--- + +⚠️ **[The Gorilla Toolkit is looking for a new maintainer](https://github.com/gorilla/mux/issues/659)** + +--- Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to their respective handler. diff --git a/mux_httpserver_test.go b/mux_httpserver_test.go index 5d2f4d3a..907ab91d 100644 --- a/mux_httpserver_test.go +++ b/mux_httpserver_test.go @@ -1,3 +1,4 @@ +//go:build go1.9 // +build go1.9 package mux From c889844abd3601217c96aabc4b2dd89f6d904c01 Mon Sep 17 00:00:00 2001 From: Michael Grigoryan <56165400+michaelgrigoryan25@users.noreply.github.com> Date: Sun, 26 Jun 2022 15:46:01 +0400 Subject: [PATCH 86/89] regexp: use iota instead of hardcoded values for regexType* (#679) * regexp: use iota instead of hardcoded values --- regexp.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/regexp.go b/regexp.go index 0144842b..37c11edc 100644 --- a/regexp.go +++ b/regexp.go @@ -22,10 +22,10 @@ type routeRegexpOptions struct { type regexpType int const ( - regexpTypePath regexpType = 0 - regexpTypeHost regexpType = 1 - regexpTypePrefix regexpType = 2 - regexpTypeQuery regexpType = 3 + regexpTypePath regexpType = iota + regexpTypeHost + regexpTypePrefix + regexpTypeQuery ) // newRouteRegexp parses a route template and returns a routeRegexp, From 07eedffb4388b4ed26b86c67aedca1e513e7553b Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Thu, 18 Aug 2022 02:19:02 +0530 Subject: [PATCH 87/89] [docs] `authenticationMiddleware` initialization in the `README.md` file (#693) * initial commit * fix `README.md` - authenticationMiddleware initialization --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7e6872b..f09a5b35 100644 --- a/README.md +++ b/README.md @@ -576,7 +576,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler r := mux.NewRouter() r.HandleFunc("/", handler) -amw := authenticationMiddleware{} +amw := authenticationMiddleware{tokenUsers: make(map[string]string)} amw.Populate() r.Use(amw.Middleware) From 5e1e8c8d45ad101414d06762b0f7f6200babc929 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 9 Dec 2022 10:56:37 -0500 Subject: [PATCH 88/89] archive mode --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f09a5b35..35aa29b6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ --- -⚠️ **[The Gorilla Toolkit is looking for a new maintainer](https://github.com/gorilla/mux/issues/659)** +The Gorilla project has been archived, and is no longer under active maintainenance. You can read more here: https://github.com/gorilla#gorilla-toolkit --- From eb99d7a67714bbab6db27f896a8c8c947b51b610 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 9 Dec 2022 10:56:57 -0500 Subject: [PATCH 89/89] archive mode --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35aa29b6..64a20e7d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ --- -The Gorilla project has been archived, and is no longer under active maintainenance. You can read more here: https://github.com/gorilla#gorilla-toolkit +**The Gorilla project has been archived, and is no longer under active maintainenance. You can read more here: https://github.com/gorilla#gorilla-toolkit** ---