bigywater.tech
Gin · 13

NoRoute 与 NoMethod 的处理链

自定义 404 页面听起来只要 r.NoRoute(handler),但 handler 链是怎么拼的、什么时候默认的 “404 page not found” 还会出现、中间件会不会也跑一遍?这篇从 serveError 的源码出发,把 NoRoute 的六种写法和执行顺序都试一遍。

1 / 6
六种 NoRoute 写法中仍输出默认正文的个数
""
NoRoute handler 里 c.FullPath() 的值
M1 → NoRoute → M1
全局中间件在 404 时的进出顺序(NoRoute 夹在中间)

默认 404 怎么来的

实测 · 本机执行输出
  gin.go 的 serveError(Gin v1.12.0):
     764 | func serveError(c *Context, code int, defaultMessage []byte) {
     765 |     c.writermem.status = code
     766 |     c.Next()
     767 |     if c.writermem.Written() {
     768 |         return
     769 |     }
     770 |     if c.writermem.Status() == code {
     771 |         c.writermem.Header()["Content-Type"] = mimePlain
     772 |         _, err := c.Writer.Write(defaultMessage)
     773 |         if err != nil {
     774 |             debugPrint("cannot write message to writer during serve error: %v", err)
     775 |         }
     776 |         return
     777 |     }
     778 |     c.writermem.WriteHeaderNow()
     779 | }
    GET    /nope      → 404 "404 page not found"  Content-Type="text/plain" X-Seen=""

serveError 先把状态设成 404,再运行 allNoRoute 这条链(没配 NoRoute 时就只有全局中间件)。链跑完以后:如果已经写了响应就什么都不做;如果还没写、状态也还是 404,才由 Gin 写上 404 page not found(text/plain);状态被改过,则只提交状态行。

NoRoute 里怎么写

实测 · 本机执行输出
    [1 只设置头,不写正文]
    GET    /nope      → 404 "404 page not found"  Content-Type="text/plain" X-Seen="1"
    [2 c.String(404, 自定义)]
    GET    /nope      → 404 "自定义:/nope 不存在"  Content-Type="text/plain; charset=utf-8" X-Seen=""
    [3 c.String(200, ...)]
    GET    /nope      → 200 "这里也有"  Content-Type="text/plain; charset=utf-8" X-Seen=""
    [4 只 c.Status(200)]
    GET    /nope      → 200 ""  Content-Type="" X-Seen=""
    [5 c.JSON(404, ...)]
    GET    /nope      → 404 "{\"error\":\"not found\",\"fullPath\":\"\",\"status\":404}"  Content-Type="application/json; charset=utf-8" X-Seen=""
    [6 c.Redirect(302, /home)]
    GET    /nope      → 302 "<a href=\"/home\">Found</a>.\n\n"  Content-Type="text/html; charset=utf-8" X-Seen=""

只设置响应头不写正文,默认正文仍会出现;写了正文就以你的为准;把状态改成 200 又不写正文,得到的是 200 的空响应(这是个容易踩的坑);c.JSON(404, …) 与 c.Redirect 都按平常方式工作。NoRoute 里 c.Writer.Status() 已经是 404,c.FullPath() 是空串——监控里要把它归到“未匹配”。

链与顺序

实测 · 本机执行输出
    先 NoRoute 后 Use: M1(进) M2(进) [NoRoute: FullPath="" Params=[] 状态=404] M2(出,状态 404) M1(出,状态 404)
    Use、NoRoute、Use 交错: M1(进) M2(进) [NoRoute] [NoRoute2] M2(出,状态 404) M1(出,状态 404)
    NoRoute 里 Abort: M1(进) [NoRoute-abort] M1(出,状态 404)

Engine.Use 与 Engine.NoRoute 都会重建 404 的 handler 链,所以谁先注册没有区别:全局中间件在前,NoRoute handler 在后。中间件的“出”半段照常执行,并且能读到 404 状态,日志中间件因此能记录 404。在 NoRoute 里 Abort 只会跳过后面的 NoRoute handler。

NoMethod 与 Static

实测 · 本机执行输出
    POST   /x         → 404 "NR"  Content-Type="text/plain; charset=utf-8" X-Seen="noroute"
    --- HandleMethodNotAllowed = true ---
    POST   /x         → 405 "NM Allow=GET"  Content-Type="text/plain; charset=utf-8" X-Seen="nomethod"
    GET    /assets/不存在.css → 404 "NR"  Content-Type="text/plain; charset=utf-8" X-Seen="noroute"
    GET    /zzz       → 404 "NR"  Content-Type="text/plain; charset=utf-8" X-Seen="noroute"

NoMethod 只有在 HandleMethodNotAllowed=true 时才会被用到。Static 目录里找不到的文件最终也是 404,走的就是同一个 NoRoute 链,所以自定义的 404 页面也会出现在静态资源缺失时。

分步演示

NoRoute 与 NoMethod
本步的对象

    本文用到的工具与边界

    • 本机 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;Static 用当前目录做根,请求的文件不存在;每种情形各一次
    • 没有验证:NoRoute 与 RedirectTrailingSlash 同时命中时哪个优先(重定向发生在 serveError 之前,本文没有单独验证);NoRoute 中 panic 时 Recovery 的行为;gin.Default() 默认带的 Logger 在 404 时的输出格式(见后面 Logger 的文章)
    京ICP备2026067502号-1京公网安备11010502063487号