Skip to content

Commit 9001d00

Browse files
nathannewyenclaude
andcommitted
fix: deduplicate HTTP methods in 405 Allow header
When multiple overlapping wildcard routes match the same path, the Allow header in 405 responses was containing duplicate HTTP methods (e.g., "POST POST POST POST" instead of just "POST"). This fix uses a map to track seen methods and only adds each method once to the Allow header. Fixes #996 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent de0d16e commit 9001d00

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

mux.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,15 @@ func (mx *Mux) updateRouteHandler() {
519519
// methods for the route.
520520
func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.ResponseWriter, r *http.Request) {
521521
return func(w http.ResponseWriter, r *http.Request) {
522+
// Deduplicate methods to avoid duplicate Allow headers when
523+
// multiple overlapping wildcard routes match the same path.
524+
// See: https://github.com/go-chi/chi/issues/996
525+
seen := make(map[methodTyp]struct{})
522526
for _, m := range methodsAllowed {
527+
if _, exists := seen[m]; exists {
528+
continue
529+
}
530+
seen[m] = struct{}{}
523531
w.Header().Add("Allow", reverseMethodMap[m])
524532
}
525533
w.WriteHeader(405)

mux_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,48 @@ func TestMethodNotAllowed(t *testing.T) {
428428
})
429429
}
430430

431+
// TestMethodNotAllowedDuplicateMethods tests that the Allow header does not
432+
// contain duplicate HTTP methods when multiple overlapping wildcard routes exist.
433+
// See: https://github.com/go-chi/chi/issues/996
434+
func TestMethodNotAllowedDuplicateMethods(t *testing.T) {
435+
r := NewRouter()
436+
437+
// Register multiple POST routes with overlapping wildcard patterns
438+
r.Post("/article/1-2-3", func(w http.ResponseWriter, r *http.Request) {
439+
w.WriteHeader(http.StatusOK)
440+
})
441+
442+
r.Post("/article/{a}", func(w http.ResponseWriter, r *http.Request) {
443+
w.WriteHeader(http.StatusOK)
444+
})
445+
446+
r.Post("/article/{b}-{c}", func(w http.ResponseWriter, r *http.Request) {
447+
w.WriteHeader(http.StatusOK)
448+
})
449+
450+
r.Post("/article/{b}-{c}-{d}", func(w http.ResponseWriter, r *http.Request) {
451+
w.WriteHeader(http.StatusOK)
452+
})
453+
454+
ts := httptest.NewServer(r)
455+
defer ts.Close()
456+
457+
// Send a GET request to a path that matches multiple wildcard patterns
458+
resp, _ := testRequest(t, ts, "GET", "/article/1-2-3", nil)
459+
if resp.StatusCode != 405 {
460+
t.Fatalf("expected status 405, got %d", resp.StatusCode)
461+
}
462+
463+
// The Allow header should only contain POST once, not multiple times
464+
allowedMethods := resp.Header.Values("Allow")
465+
if len(allowedMethods) != 1 {
466+
t.Fatalf("Allow header should contain exactly 1 method (POST), got %d: %v", len(allowedMethods), allowedMethods)
467+
}
468+
if allowedMethods[0] != "POST" {
469+
t.Fatalf("Allow header should be POST, got %s", allowedMethods[0])
470+
}
471+
}
472+
431473
func TestMuxNestedMethodNotAllowed(t *testing.T) {
432474
r := NewRouter()
433475
r.Get("/root", func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)