给 /x 注册了 GET 和 POST,然后 DELETE 它,Gin 默认返回 404,而不是 405。这篇打开 HandleMethodNotAllowed,看它怎么判定、Allow 怎么拼、顺序由什么决定,以及自定义的 NoMethod 页面能拿到什么。
=== HandleMethodNotAllowed = false ===
GET /x → 200 "ok"
DELETE /x → 404 "404 page not found"
PATCH /y/5 → 404 "404 page not found"
GET /y/5 → 404 "404 page not found"
HEAD /x → 404 "404 page not found"
OPTIONS /x → 404 "404 page not found"
DELETE /nothing → 404 "404 page not found"
=== HandleMethodNotAllowed = true ===
GET /x → 200 "ok"
DELETE /x → 405 "405 method not allowed" Allow: GET, POST
PATCH /y/5 → 405 "405 method not allowed" Allow: PUT
GET /y/5 → 405 "405 method not allowed" Allow: PUT
HEAD /x → 405 "405 method not allowed" Allow: GET, POST
OPTIONS /x → 405 "405 method not allowed" Allow: GET, POST
DELETE /nothing → 404 "404 page not found"HTTP 语义上“路径存在但方法不允许”应该是 405,但 Gin 默认关着这个功能。想要 405 就得 r.HandleMethodNotAllowed = true。注意 HEAD 和 OPTIONS 也不会被自动处理:没有显式注册 HEAD 的路由,HEAD 请求得到的是 405(打开时)或 404(默认)。
gin.go 的 handleHTTPRequest 里 405 的部分(Gin v1.12.0):
738 | if engine.HandleMethodNotAllowed && len(t) > 0 {
739 | // According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
740 | // containing a list of the target resource's currently supported methods.
741 | allowed := make([]string, 0, len(t)-1)
742 | for _, tree := range engine.trees {
743 | if tree.method == httpMethod {
744 | continue
745 | }
746 | if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
747 | allowed = append(allowed, tree.method)
748 | }
749 | }
750 | if len(allowed) > 0 {
751 | c.handlers = engine.allNoMethod
752 | c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
753 | serveError(c, http.StatusMethodNotAllowed, default405Body)
754 | return
755 | }
756 | }
757 | 先在当前方法的树里找,找不到(也没触发重定向)才进入 405 的判断:遍历其它所有方法的树,看哪棵能匹配当前路径,匹配到的方法名组成 Allow。没有任何一棵能匹配就仍然是 404。这个遍历只发生在会返回 404/405 的请求上。
注册顺序 [GET POST PUT]:
OPTIONS /res → 405 "405 method not allowed" Allow: GET, POST, PUT
r.TreeMethods() = [GET POST PUT]
注册顺序 [PUT POST GET]:
OPTIONS /res → 405 "405 method not allowed" Allow: PUT, POST, GET
r.TreeMethods() = [PUT POST GET]
注册顺序 [DELETE PATCH GET POST]:
OPTIONS /res → 405 "405 method not allowed" Allow: DELETE, PATCH, GET, POST
r.TreeMethods() = [DELETE PATCH GET POST]顺序就是 engine.trees 的顺序,也就是每个 HTTP 方法第一次被注册的顺序,不排序、不去重。RFC 没有要求 Allow 有序,所以这只是个实现细节,但写测试断言 Allow 的字符串时要知道它取决于注册顺序。
DELETE /x → 405 "{\"allow\":\"GET\",\"error\":\"method not allowed\"}" Allow: GET
DELETE /dir/ → 405 "{\"allow\":\"POST\",\"error\":\"method not allowed\"}" Allow: POST
DELETE /dir → 404 "404 page not found"
GET /dir → 404 "404 page not found"
GET /dir/ → 405 "{\"allow\":\"POST\",\"error\":\"method not allowed\"}" Allow: POSTr.NoMethod(handler) 可以自定义 405 的响应体,进入 handler 时 Allow 头已经设好。405 只做精确匹配:路由是 POST /dir/ 时,DELETE /dir 因为路径少了斜杠,得到的是 404,而不是 405 或重定向。