Skip to content

Commit 4b14b83

Browse files
authored
Adds the Allow header on 405 response (#776)
1 parent 7f28096 commit 4b14b83

File tree

4 files changed

+79
-7
lines changed

4 files changed

+79
-7
lines changed

context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type Context struct {
7676

7777
// methodNotAllowed hint
7878
methodNotAllowed bool
79+
methodsAllowed []methodTyp // allowed methods in case of a 405
7980
}
8081

8182
// Reset a routing context to its initial state.

mux.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,11 @@ func (mx *Mux) NotFoundHandler() http.HandlerFunc {
378378

379379
// MethodNotAllowedHandler returns the default Mux 405 responder whenever
380380
// a method cannot be resolved for a route.
381-
func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc {
381+
func (mx *Mux) MethodNotAllowedHandler(methodsAllowed ...methodTyp) http.HandlerFunc {
382382
if mx.methodNotAllowedHandler != nil {
383383
return mx.methodNotAllowedHandler
384384
}
385-
return methodNotAllowedHandler
385+
return methodNotAllowedHandler(methodsAllowed...)
386386
}
387387

388388
// handle registers a http.Handler in the routing tree for a particular http method
@@ -445,7 +445,7 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
445445
return
446446
}
447447
if rctx.methodNotAllowed {
448-
mx.MethodNotAllowedHandler().ServeHTTP(w, r)
448+
mx.MethodNotAllowedHandler(rctx.methodsAllowed...).ServeHTTP(w, r)
449449
} else {
450450
mx.NotFoundHandler().ServeHTTP(w, r)
451451
}
@@ -480,8 +480,14 @@ func (mx *Mux) updateRouteHandler() {
480480
}
481481

482482
// methodNotAllowedHandler is a helper function to respond with a 405,
483-
// method not allowed.
484-
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
485-
w.WriteHeader(405)
486-
w.Write(nil)
483+
// method not allowed. It sets the Allow header with the list of allowed
484+
// methods for the route.
485+
func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.ResponseWriter, r *http.Request) {
486+
return func(w http.ResponseWriter, r *http.Request) {
487+
for _, m := range methodsAllowed {
488+
w.Header().Add("Allow", reverseMethodMap[m])
489+
}
490+
w.WriteHeader(405)
491+
w.Write(nil)
492+
}
487493
}

mux_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,43 @@ func TestMuxNestedNotFound(t *testing.T) {
392392
}
393393
}
394394

395+
func TestMethodNotAllowed(t *testing.T) {
396+
r := NewRouter()
397+
398+
r.Get("/hi", func(w http.ResponseWriter, r *http.Request) {
399+
w.Write([]byte("hi, get"))
400+
})
401+
402+
r.Head("/hi", func(w http.ResponseWriter, r *http.Request) {
403+
w.Write([]byte("hi, head"))
404+
})
405+
406+
ts := httptest.NewServer(r)
407+
defer ts.Close()
408+
409+
t.Run("Registered Method", func(t *testing.T) {
410+
resp, _ := testRequest(t, ts, "GET", "/hi", nil)
411+
if resp.StatusCode != 200 {
412+
t.Fatal(resp.Status)
413+
}
414+
if resp.Header.Values("Allow") != nil {
415+
t.Fatal("allow should be empty when method is registered")
416+
}
417+
})
418+
419+
t.Run("Unregistered Method", func(t *testing.T) {
420+
resp, _ := testRequest(t, ts, "POST", "/hi", nil)
421+
if resp.StatusCode != 405 {
422+
t.Fatal(resp.Status)
423+
}
424+
allowedMethods := resp.Header.Values("Allow")
425+
if len(allowedMethods) != 2 || allowedMethods[0] != "GET" || allowedMethods[1] != "HEAD" {
426+
t.Fatal("Allow header should contain 2 headers: GET, HEAD. Received: ", allowedMethods)
427+
428+
}
429+
})
430+
}
431+
395432
func TestMuxNestedMethodNotAllowed(t *testing.T) {
396433
r := NewRouter()
397434
r.Get("/root", func(w http.ResponseWriter, r *http.Request) {
@@ -1771,6 +1808,7 @@ func BenchmarkMux(b *testing.B) {
17711808
mx := NewRouter()
17721809
mx.Get("/", h1)
17731810
mx.Get("/hi", h2)
1811+
mx.Post("/hi-post", h2) // used to benchmark 405 responses
17741812
mx.Get("/sup/{id}/and/{this}", h3)
17751813
mx.Get("/sup/{id}/{bar:foo}/{this}", h3)
17761814

@@ -1787,6 +1825,7 @@ func BenchmarkMux(b *testing.B) {
17871825
routes := []string{
17881826
"/",
17891827
"/hi",
1828+
"/hi-post",
17901829
"/sup/123/and/this",
17911830
"/sup/123/foo/this",
17921831
"/sharing/z/aBc", // subrouter-1

tree.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ var methodMap = map[string]methodTyp{
4343
http.MethodTrace: mTRACE,
4444
}
4545

46+
var reverseMethodMap = map[methodTyp]string{
47+
mCONNECT: http.MethodConnect,
48+
mDELETE: http.MethodDelete,
49+
mGET: http.MethodGet,
50+
mHEAD: http.MethodHead,
51+
mOPTIONS: http.MethodOptions,
52+
mPATCH: http.MethodPatch,
53+
mPOST: http.MethodPost,
54+
mPUT: http.MethodPut,
55+
mTRACE: http.MethodTrace,
56+
}
57+
4658
// RegisterMethod adds support for custom HTTP method handlers, available
4759
// via Router#Method and Router#MethodFunc
4860
func RegisterMethod(method string) {
@@ -454,6 +466,13 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
454466
return xn
455467
}
456468

469+
for endpoints := range xn.endpoints {
470+
if endpoints == mALL || endpoints == mSTUB {
471+
continue
472+
}
473+
rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints)
474+
}
475+
457476
// flag that the routing context found a route, but not a corresponding
458477
// supported method
459478
rctx.methodNotAllowed = true
@@ -493,6 +512,13 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
493512
return xn
494513
}
495514

515+
for endpoints := range xn.endpoints {
516+
if endpoints == mALL || endpoints == mSTUB {
517+
continue
518+
}
519+
rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints)
520+
}
521+
496522
// flag that the routing context found a route, but not a corresponding
497523
// supported method
498524
rctx.methodNotAllowed = true

0 commit comments

Comments
 (0)