From 50a3f777c02181e968cbfad6f518e81168136b1c Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 17:06:20 +0800 Subject: [PATCH] fix: merge methods when Route.Methods is called repeatedly Chaining Methods() on the same route previously added independent matchers, requiring every method to match the same request and causing 405 responses for valid verbs like PUT and PATCH. Merge repeated calls into a single method matcher and aggregate methods in GetMethods(). Fixes gorilla/mux#694 --- mux_test.go | 3 +-- route.go | 32 +++++++++++++++++++++++++++++--- route_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/mux_test.go b/mux_test.go index bac758bc..af083cb1 100644 --- a/mux_test.go +++ b/mux_test.go @@ -2724,9 +2724,8 @@ func TestSubrouterMatching(t *testing.T) { r.Methods("POST").Subrouter().Methods("GET") }, []request{ - {"matches before", newRequest("POST", "/"), none}, + {"matches merged methods", newRequest("GET", "/"), stdOnly}, {"no match other", newRequest("HEAD", "/"), none}, - {"matches override", newRequest("GET", "/"), none}, }, }, { diff --git a/route.go b/route.go index d10401e9..79a2ba31 100644 --- a/route.go +++ b/route.go @@ -395,13 +395,31 @@ func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { // Methods adds a matcher for HTTP methods. // It accepts a sequence of one or more methods to be matched, e.g.: // "GET", "POST", "PUT". +// +// Repeated calls merge into the same matcher instead of requiring every method +// to match independently. func (r *Route) Methods(methods ...string) *Route { for k, v := range methods { methods[k] = strings.ToUpper(v) } + for i, m := range r.matchers { + if existing, ok := m.(methodMatcher); ok { + r.matchers[i] = mergeMethodMatcher(existing, methods...) + return r + } + } return r.addMatcher(methodMatcher(methods)) } +func mergeMethodMatcher(existing methodMatcher, methods ...string) methodMatcher { + for _, method := range methods { + if !matchInArray(existing, method) { + existing = append(existing, method) + } + } + return existing +} + // Path ----------------------------------------------------------------------- // Path adds a matcher for the URL path. @@ -768,12 +786,20 @@ func (r *Route) GetMethods() ([]string, error) { if r.err != nil { return nil, r.err } + var methods []string for _, m := range r.matchers { - if methods, ok := m.(methodMatcher); ok { - return []string(methods), nil + if mm, ok := m.(methodMatcher); ok { + for _, method := range mm { + if !matchInArray(methods, method) { + methods = append(methods, method) + } + } } } - return nil, errors.New("mux: route doesn't have methods") + if len(methods) == 0 { + return nil, errors.New("mux: route doesn't have methods") + } + return methods, nil } // GetHostTemplate returns the template used to build the diff --git a/route_test.go b/route_test.go index 283c3ad5..f54c10d4 100644 --- a/route_test.go +++ b/route_test.go @@ -152,3 +152,41 @@ func TestRouteMetadata(t *testing.T) { router.ServeHTTP(rw, req) }) } + +func TestRouteMethodsRepeatedCalls(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + route := NewRouter().NewRoute() + route.Path("/test").Handler(handler).Methods("PUT").Methods("PATCH") + route.Methods("OPTIONS") + + methods, err := route.GetMethods() + if err != nil { + t.Fatalf("GetMethods failed: %v", err) + } + if len(methods) != 3 { + t.Fatalf("expected 3 methods, got %v", methods) + } + + for _, method := range []string{"PUT", "PATCH", "OPTIONS"} { + req := newRequest(method, "/test") + match := &RouteMatch{} + if !route.Match(req, match) { + t.Fatalf("%s should match route", method) + } + if match.MatchErr != nil { + t.Fatalf("%s match returned error: %v", method, match.MatchErr) + } + } + + req := newRequest("GET", "/test") + match := &RouteMatch{} + if route.Match(req, match) { + t.Fatal("GET should not match route") + } + if match.MatchErr != ErrMethodMismatch { + t.Fatalf("expected ErrMethodMismatch, got %v", match.MatchErr) + } +}