diff --git a/mux.go b/mux.go index 71652dd1..b7293d1d 100644 --- a/mux.go +++ b/mux.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "slices" "strings" "sync" ) @@ -516,8 +517,12 @@ 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. +// methods for the route. It deduplicates methods that may appear multiple +// times when overlapping wildcard routes match the same path. func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.ResponseWriter, r *http.Request) { + slices.Sort(methodsAllowed) + methodsAllowed = slices.Compact(methodsAllowed) + return func(w http.ResponseWriter, r *http.Request) { for _, m := range methodsAllowed { w.Header().Add("Allow", reverseMethodMap[m]) diff --git a/mux_test.go b/mux_test.go index d69a6f8a..e1eff79e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -428,6 +428,39 @@ func TestMethodNotAllowed(t *testing.T) { }) } +func TestMethodNotAllowedDuplicateAllow(t *testing.T) { + r := NewRouter() + + // Register multiple overlapping wildcard routes with the same method. + // When a request uses an unsupported method, the Allow header should + // list each method only once, not once per matching route. + 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, got %d", resp.StatusCode) + } + + allowedMethods := resp.Header.Values("Allow") + if len(allowedMethods) != 1 || allowedMethods[0] != "POST" { + t.Fatalf("Allow header should contain exactly one POST entry, got: %v", allowedMethods) + } +} + func TestMuxNestedMethodNotAllowed(t *testing.T) { r := NewRouter() r.Get("/root", func(w http.ResponseWriter, r *http.Request) {