bigywater.tech
Gin · 12

HandleMethodNotAllowed:405 怎么判定

给 /x 注册了 GET 和 POST,然后 DELETE 它,Gin 默认返回 404,而不是 405。这篇打开 HandleMethodNotAllowed,看它怎么判定、Allow 怎么拼、顺序由什么决定,以及自定义的 NoMethod 页面能拿到什么。

404 → 405
打开 HandleMethodNotAllowed 之后 DELETE /x 的状态码
GET, POST
DELETE /x 的 Allow 头
PUT, POST, GET
先注册 PUT 再 POST 再 GET 时的 Allow

默认是 404

实测 · 本机执行输出
    === 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 的请求上。

Allow 的顺序

实测 · 本机执行输出
    注册顺序 [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 的字符串时要知道它取决于注册顺序。

NoMethod 与边界

实测 · 本机执行输出
    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: POST

r.NoMethod(handler) 可以自定义 405 的响应体,进入 handler 时 Allow 头已经设好。405 只做精确匹配:路由是 POST /dir/ 时,DELETE /dir 因为路径少了斜杠,得到的是 404,而不是 405 或重定向。

分步演示

405 判定
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 直接调用 ServeHTTP;TreeMethods 是源码副本里加的只读函数(zz_dump.go);每种情形各一次
    • 没有验证:Allow 里是否应该出现 HEAD(Gin 的行为是不自动加,没有对照其它框架);大量方法树(自定义方法)时 405 判定的耗时;NoMethod 与全局中间件的交互;CORS 预检请求(OPTIONS)在 405 打开时的处理(后面讲自定义方法的文章会再提)
    京ICP备2026067502号-1京公网安备11010502063487号