Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type Context struct {
// patterns across a stack of sub-routers.
RoutePatterns []string

methodsAllowed []methodTyp // allowed methods in case of a 405
methodsAllowed methodTyp // allowed methods bitmask in case of a 405
methodNotAllowed bool
}

Expand All @@ -91,7 +91,7 @@ func (x *Context) Reset() {
x.routeParams.Keys = x.routeParams.Keys[:0]
x.routeParams.Values = x.routeParams.Values[:0]
x.methodNotAllowed = false
x.methodsAllowed = x.methodsAllowed[:0]
x.methodsAllowed = 0
x.parentCtx = nil
}

Expand Down
19 changes: 11 additions & 8 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,11 @@ func (mx *Mux) NotFoundHandler() http.HandlerFunc {

// MethodNotAllowedHandler returns the default Mux 405 responder whenever
// a method cannot be resolved for a route.
func (mx *Mux) MethodNotAllowedHandler(methodsAllowed ...methodTyp) http.HandlerFunc {
func (mx *Mux) MethodNotAllowedHandler(methodsAllowed methodTyp) http.HandlerFunc {
if mx.methodNotAllowedHandler != nil {
return mx.methodNotAllowedHandler
}
return methodNotAllowedHandler(methodsAllowed...)
return methodNotAllowedHandler(methodsAllowed)
}

// handle registers a http.Handler in the routing tree for a particular http method
Expand Down Expand Up @@ -467,7 +467,7 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
}
method, ok := methodMap[rctx.RouteMethod]
if !ok {
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
mx.MethodNotAllowedHandler(0).ServeHTTP(w, r)
return
}

Expand All @@ -484,7 +484,7 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if rctx.methodNotAllowed {
mx.MethodNotAllowedHandler(rctx.methodsAllowed...).ServeHTTP(w, r)
mx.MethodNotAllowedHandler(rctx.methodsAllowed).ServeHTTP(w, r)
} else {
mx.NotFoundHandler().ServeHTTP(w, r)
}
Expand Down Expand Up @@ -520,11 +520,14 @@ func (mx *Mux) updateRouteHandler() {

// methodNotAllowedHandler is a helper function to respond with a 405,
// method not allowed. It sets the Allow header with the list of allowed
// methods for the route.
func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.ResponseWriter, r *http.Request) {
// methods for the route. methodsAllowed is a bitmask of methodTyp values,
// ensuring each HTTP method appears at most once in the Allow header.
func methodNotAllowedHandler(methodsAllowed methodTyp) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
for _, m := range methodsAllowed {
w.Header().Add("Allow", reverseMethodMap[m])
for m, name := range reverseMethodMap {
if methodsAllowed&m != 0 {
w.Header().Add("Allow", name)
}
}
w.WriteHeader(405)
w.Write(nil)
Expand Down
44 changes: 44 additions & 0 deletions mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,50 @@ func TestMethodNotAllowed(t *testing.T) {
})
}

// TestMethodNotAllowedNoDuplicates verifies that overlapping parameterized routes
// do not produce duplicate HTTP method names in the 405 Allow response header.
// See https://github.com/go-chi/chi/issues/996
func TestMethodNotAllowedNoDuplicates(t *testing.T) {
r := NewRouter()

// Multiple overlapping param patterns all registered for POST.
// A GET request to /article/1-2-3 should return 405 with Allow: POST (once).
r.Post("/article/1-2-3", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Post("/article/{a}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Post("/article/{b}-{c}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Post("/article/{b}-{c}-{d}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

ts := httptest.NewServer(r)
defer ts.Close()

resp, _ := testRequest(t, ts, "GET", "/article/1-2-3", nil)
if resp.StatusCode != 405 {
t.Fatalf("expected 405 Method Not Allowed, got %d", resp.StatusCode)
}

allowValues := resp.Header.Values("Allow")
seen := make(map[string]int)
for _, v := range allowValues {
seen[v]++
}
for method, count := range seen {
if count > 1 {
t.Errorf("Allow header contains %q %d times; want exactly 1", method, count)
}
}
if _, ok := seen["POST"]; !ok {
t.Errorf("Allow header missing POST; got %v", allowValues)
}
}

func TestMuxNestedMethodNotAllowed(t *testing.T) {
r := NewRouter()
r.Get("/root", func(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 2 additions & 2 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
if endpoints == mALL || endpoints == mSTUB {
continue
}
rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints)
rctx.methodsAllowed |= endpoints
}

// flag that the routing context found a route, but not a corresponding
Expand Down Expand Up @@ -524,7 +524,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
if endpoints == mALL || endpoints == mSTUB {
continue
}
rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints)
rctx.methodsAllowed |= endpoints
}

// flag that the routing context found a route, but not a corresponding
Expand Down