Skip to content

Commit 6192f46

Browse files
committed
net/http: handle MethodNotAllowed
If no pattern matches a request, but a pattern would have matched if the request had a different method, then serve a 405 (Method Not Allowed), and populate the "Allow" header with the methods that would have succeeded. Updates #61640. Change-Id: I0ae9eb95e62c71ff7766a03043525a97099ac1bb Reviewed-on: https://go-review.googlesource.com/c/go/+/528401 Reviewed-by: Damien Neil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 11b08a7 commit 6192f46

File tree

4 files changed

+177
-6
lines changed

4 files changed

+177
-6
lines changed

src/net/http/request_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"io"
1616
"math"
1717
"mime/multipart"
18+
"net/http"
1819
. "net/http"
1920
"net/http/httptest"
2021
"net/url"
@@ -1473,10 +1474,11 @@ func TestPathValue(t *testing.T) {
14731474
})
14741475
server := httptest.NewServer(mux)
14751476
defer server.Close()
1476-
_, err := Get(server.URL + test.url)
1477+
res, err := Get(server.URL + test.url)
14771478
if err != nil {
14781479
t.Fatal(err)
14791480
}
1481+
res.Body.Close()
14801482
}
14811483
}
14821484

@@ -1499,8 +1501,57 @@ func TestSetPathValue(t *testing.T) {
14991501
})
15001502
server := httptest.NewServer(mux)
15011503
defer server.Close()
1502-
_, err := Get(server.URL + "/a/b/c/d/e")
1504+
res, err := Get(server.URL + "/a/b/c/d/e")
15031505
if err != nil {
15041506
t.Fatal(err)
15051507
}
1508+
res.Body.Close()
1509+
}
1510+
1511+
func TestStatus(t *testing.T) {
1512+
// The main purpose of this test is to check 405 responses and the Allow header.
1513+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
1514+
mux := NewServeMux()
1515+
mux.Handle("GET /g", h)
1516+
mux.Handle("POST /p", h)
1517+
mux.Handle("PATCH /p", h)
1518+
mux.Handle("PUT /r", h)
1519+
mux.Handle("GET /r/", h)
1520+
server := httptest.NewServer(mux)
1521+
defer server.Close()
1522+
1523+
for _, test := range []struct {
1524+
method, path string
1525+
wantStatus int
1526+
wantAllow string
1527+
}{
1528+
{"GET", "/g", 200, ""},
1529+
{"HEAD", "/g", 200, ""},
1530+
{"POST", "/g", 405, "GET, HEAD"},
1531+
{"GET", "/x", 404, ""},
1532+
{"GET", "/p", 405, "PATCH, POST"},
1533+
{"GET", "/./p", 405, "PATCH, POST"},
1534+
{"GET", "/r/", 200, ""},
1535+
{"GET", "/r", 200, ""}, // redirected
1536+
{"HEAD", "/r/", 200, ""},
1537+
{"HEAD", "/r", 200, ""}, // redirected
1538+
{"PUT", "/r/", 405, "GET, HEAD"},
1539+
{"PUT", "/r", 200, ""},
1540+
} {
1541+
req, err := http.NewRequest(test.method, server.URL+test.path, nil)
1542+
if err != nil {
1543+
t.Fatal(err)
1544+
}
1545+
res, err := http.DefaultClient.Do(req)
1546+
if err != nil {
1547+
t.Fatal(err)
1548+
}
1549+
res.Body.Close()
1550+
if g, w := res.StatusCode, test.wantStatus; g != w {
1551+
t.Errorf("%s %s: got %d, want %d", test.method, test.path, g, w)
1552+
}
1553+
if g, w := res.Header.Get("Allow"), test.wantAllow; g != w {
1554+
t.Errorf("%s %s, Allow: got %q, want %q", test.method, test.path, g, w)
1555+
}
1556+
}
15061557
}

src/net/http/routing_tree.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,30 @@ func firstSegment(path string) (seg, rest string) {
220220
}
221221
return path[:i], path[i:]
222222
}
223+
224+
// matchingMethods adds to methodSet all the methods that would result in a
225+
// match if passed to routingNode.match with the given host and path.
226+
func (root *routingNode) matchingMethods(host, path string, methodSet map[string]bool) {
227+
if host != "" {
228+
root.findChild(host).matchingMethodsPath(path, methodSet)
229+
}
230+
root.emptyChild.matchingMethodsPath(path, methodSet)
231+
if methodSet["GET"] {
232+
methodSet["HEAD"] = true
233+
}
234+
}
235+
236+
func (n *routingNode) matchingMethodsPath(path string, set map[string]bool) {
237+
if n == nil {
238+
return
239+
}
240+
n.children.eachPair(func(method string, c *routingNode) bool {
241+
if p, _ := c.matchPath(path, nil); p != nil {
242+
set[method] = true
243+
}
244+
return true
245+
})
246+
// Don't look at the empty child. If there were an empty
247+
// child, it would match on any method, but we only
248+
// call this when we fail to match on a method.
249+
}

src/net/http/routing_tree_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,65 @@ func TestRoutingNodeMatch(t *testing.T) {
209209
})
210210
}
211211

212+
func TestMatchingMethods(t *testing.T) {
213+
hostTree := buildTree("GET a.com/", "PUT b.com/", "POST /foo/{x}")
214+
for _, test := range []struct {
215+
name string
216+
tree *routingNode
217+
host, path string
218+
want string
219+
}{
220+
{
221+
"post",
222+
buildTree("POST /"), "", "/foo",
223+
"POST",
224+
},
225+
{
226+
"get",
227+
buildTree("GET /"), "", "/foo",
228+
"GET,HEAD",
229+
},
230+
{
231+
"host",
232+
hostTree, "", "/foo",
233+
"",
234+
},
235+
{
236+
"host",
237+
hostTree, "", "/foo/bar",
238+
"POST",
239+
},
240+
{
241+
"host2",
242+
hostTree, "a.com", "/foo/bar",
243+
"GET,HEAD,POST",
244+
},
245+
{
246+
"host3",
247+
hostTree, "b.com", "/bar",
248+
"PUT",
249+
},
250+
{
251+
// This case shouldn't come up because we only call matchingMethods
252+
// when there was no match, but we include it for completeness.
253+
"empty",
254+
buildTree("/"), "", "/",
255+
"",
256+
},
257+
} {
258+
t.Run(test.name, func(t *testing.T) {
259+
ms := map[string]bool{}
260+
test.tree.matchingMethods(test.host, test.path, ms)
261+
keys := mapKeys(ms)
262+
sort.Strings(keys)
263+
got := strings.Join(keys, ",")
264+
if got != test.want {
265+
t.Errorf("got %s, want %s", got, test.want)
266+
}
267+
})
268+
}
269+
}
270+
212271
func (n *routingNode) print(w io.Writer, level int) {
213272
indent := strings.Repeat(" ", level)
214273
if n.pattern != nil {

src/net/http/server.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
urlpkg "net/url"
2424
"path"
2525
"runtime"
26+
"sort"
2627
"strconv"
2728
"strings"
2829
"sync"
@@ -2423,13 +2424,13 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *patte
24232424
// TODO(jba): use escaped path. This is an independent change that is also part
24242425
// of proposal https://go.dev/issue/61410.
24252426
path := r.URL.Path
2426-
2427+
host := r.URL.Host
24272428
// CONNECT requests are not canonicalized.
24282429
if r.Method == "CONNECT" {
24292430
// If r.URL.Path is /tree and its handler is not registered,
24302431
// the /tree -> /tree/ redirect applies to CONNECT requests
24312432
// but the path canonicalization does not.
2432-
_, _, u := mux.matchOrRedirect(r.URL.Host, r.Method, path, r.URL)
2433+
_, _, u := mux.matchOrRedirect(host, r.Method, path, r.URL)
24332434
if u != nil {
24342435
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
24352436
}
@@ -2439,7 +2440,7 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *patte
24392440
} else {
24402441
// All other requests have any port stripped and path cleaned
24412442
// before passing to mux.handler.
2442-
host := stripHostPort(r.Host)
2443+
host = stripHostPort(r.Host)
24432444
path = cleanPath(path)
24442445

24452446
// If the given path is /tree and its handler is not registered,
@@ -2460,7 +2461,16 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *patte
24602461
}
24612462
}
24622463
if n == nil {
2463-
// TODO(jba): support 405 (MethodNotAllowed) by checking for patterns with different methods.
2464+
// We didn't find a match with the request method. To distinguish between
2465+
// Not Found and Method Not Allowed, see if there is another pattern that
2466+
// matches except for the method.
2467+
allowedMethods := mux.matchingMethods(host, path)
2468+
if len(allowedMethods) > 0 {
2469+
return HandlerFunc(func(w ResponseWriter, r *Request) {
2470+
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
2471+
Error(w, StatusText(StatusMethodNotAllowed), StatusMethodNotAllowed)
2472+
}), "", nil, nil
2473+
}
24642474
return NotFoundHandler(), "", nil, nil
24652475
}
24662476
return n.handler, n.pattern.String(), n.pattern, matches
@@ -2542,6 +2552,30 @@ func exactMatch(n *routingNode, path string) bool {
25422552
return len(n.pattern.segments) == strings.Count(path, "/")
25432553
}
25442554

2555+
// matchingMethods return a sorted list of all methods that would match with the given host and path.
2556+
func (mux *ServeMux) matchingMethods(host, path string) []string {
2557+
// Hold the read lock for the entire method so that the two matches are done
2558+
// on the same set of registered patterns.
2559+
mux.mu.RLock()
2560+
defer mux.mu.RUnlock()
2561+
ms := map[string]bool{}
2562+
mux.tree.matchingMethods(host, path, ms)
2563+
// matchOrRedirect will try appending a trailing slash if there is no match.
2564+
mux.tree.matchingMethods(host, path+"/", ms)
2565+
methods := mapKeys(ms)
2566+
sort.Strings(methods)
2567+
return methods
2568+
}
2569+
2570+
// TODO: replace with maps.Keys when it is defined.
2571+
func mapKeys[K comparable, V any](m map[K]V) []K {
2572+
var ks []K
2573+
for k := range m {
2574+
ks = append(ks, k)
2575+
}
2576+
return ks
2577+
}
2578+
25452579
// ServeHTTP dispatches the request to the handler whose
25462580
// pattern most closely matches the request URL.
25472581
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {

0 commit comments

Comments
 (0)