Skip to content

Commit a659b61

Browse files
Mayank Patelkisielk
authored andcommitted
Fix #271: Return 405 instead of 404 when request method doesn't match the route
1 parent ac112f7 commit a659b61

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

mux.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import (
1313
"strings"
1414
)
1515

16+
var (
17+
ErrMethodMismatch = errors.New("method is not allowed")
18+
)
19+
1620
// NewRouter returns a new router instance.
1721
func NewRouter() *Router {
1822
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false}
@@ -39,6 +43,10 @@ func NewRouter() *Router {
3943
type Router struct {
4044
// Configurable Handler to be used when no route matches.
4145
NotFoundHandler http.Handler
46+
47+
// Configurable Handler to be used when the request method does not match the route.
48+
MethodNotAllowedHandler http.Handler
49+
4250
// Parent route, if this is a subrouter.
4351
parent parentRoute
4452
// Routes to be matched, in order.
@@ -65,6 +73,11 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
6573
}
6674
}
6775

76+
if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil {
77+
match.Handler = r.MethodNotAllowedHandler
78+
return true
79+
}
80+
6881
// Closest match for a router (includes sub-routers)
6982
if r.NotFoundHandler != nil {
7083
match.Handler = r.NotFoundHandler
@@ -105,9 +118,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
105118
req = setVars(req, match.Vars)
106119
req = setCurrentRoute(req, match.Route)
107120
}
121+
122+
if handler == nil && match.MatchErr == ErrMethodMismatch {
123+
handler = methodNotAllowedHandler()
124+
}
125+
108126
if handler == nil {
109127
handler = http.NotFoundHandler()
110128
}
129+
111130
if !r.KeepContext {
112131
defer contextClear(req)
113132
}
@@ -344,6 +363,11 @@ type RouteMatch struct {
344363
Route *Route
345364
Handler http.Handler
346365
Vars map[string]string
366+
367+
// MatchErr is set to appropriate matching error
368+
// It is set to ErrMethodMismatch if there is a mismatch in
369+
// the request method and route method
370+
MatchErr error
347371
}
348372

349373
type contextKey int
@@ -545,3 +569,12 @@ func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]s
545569
}
546570
return true
547571
}
572+
573+
// methodNotAllowed replies to the request with an HTTP status code 405.
574+
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
575+
w.WriteHeader(http.StatusMethodNotAllowed)
576+
}
577+
578+
// methodNotAllowedHandler returns a simple request handler
579+
// that replies to each request with a status code 405.
580+
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }

mux_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,3 +1871,42 @@ func newRequest(method, url string) *http.Request {
18711871
}
18721872
return req
18731873
}
1874+
1875+
func TestNoMatchMethodErrorHandler(t *testing.T) {
1876+
func1 := func(w http.ResponseWriter, r *http.Request) {}
1877+
1878+
r := NewRouter()
1879+
r.HandleFunc("/", func1).Methods("GET", "POST")
1880+
1881+
req, _ := http.NewRequest("PUT", "http://localhost/", nil)
1882+
match := new(RouteMatch)
1883+
matched := r.Match(req, match)
1884+
1885+
if matched {
1886+
t.Error("Should not have matched route for methods")
1887+
}
1888+
1889+
if match.MatchErr != ErrMethodMismatch {
1890+
t.Error("Should get ErrMethodMismatch error")
1891+
}
1892+
1893+
resp := NewRecorder()
1894+
r.ServeHTTP(resp, req)
1895+
if resp.Code != 405 {
1896+
t.Errorf("Expecting code %v", 405)
1897+
}
1898+
1899+
//Add matching route now
1900+
r.HandleFunc("/", func1).Methods("PUT")
1901+
1902+
match = new(RouteMatch)
1903+
matched = r.Match(req, match)
1904+
1905+
if !matched {
1906+
t.Error("Should have matched route for methods")
1907+
}
1908+
1909+
if match.MatchErr != nil {
1910+
t.Error("Should not have any matching error. Found:", match.MatchErr)
1911+
}
1912+
}

old_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,7 @@ func TestRouteMatchers(t *testing.T) {
121121
var routeMatch RouteMatch
122122
matched := router.Match(request, &routeMatch)
123123
if matched != shouldMatch {
124-
// Need better messages. :)
125-
if matched {
126-
t.Errorf("Should match.")
127-
} else {
128-
t.Errorf("Should not match.")
129-
}
124+
t.Errorf("Expected: %v\nGot: %v\nRequest: %v %v", shouldMatch, matched, request.Method, url)
130125
}
131126

132127
if matched {
@@ -188,7 +183,6 @@ func TestRouteMatchers(t *testing.T) {
188183
match(true)
189184

190185
// 2nd route --------------------------------------------------------------
191-
192186
// Everything match.
193187
reset2()
194188
match(true)

route.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,27 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
5252
if r.buildOnly || r.err != nil {
5353
return false
5454
}
55+
56+
var matchErr error
57+
5558
// Match everything.
5659
for _, m := range r.matchers {
5760
if matched := m.Match(req, match); !matched {
61+
if _, ok := m.(methodMatcher); ok {
62+
matchErr = ErrMethodMismatch
63+
continue
64+
}
65+
matchErr = nil
5866
return false
5967
}
6068
}
69+
70+
if matchErr != nil {
71+
match.MatchErr = matchErr
72+
return false
73+
}
74+
75+
match.MatchErr = nil
6176
// Yay, we have a match. Let's collect some info about it.
6277
if match.Route == nil {
6378
match.Route = r
@@ -68,6 +83,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
6883
if match.Vars == nil {
6984
match.Vars = make(map[string]string)
7085
}
86+
7187
// Set variables.
7288
if r.regexp != nil {
7389
r.regexp.setMatch(req, match, r)

0 commit comments

Comments
 (0)