自定义 404 页面听起来只要 r.NoRoute(handler),但 handler 链是怎么拼的、什么时候默认的 “404 page not found” 还会出现、中间件会不会也跑一遍?这篇从 serveError 的源码出发,把 NoRoute 的六种写法和执行顺序都试一遍。
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);状态被改过,则只提交状态行。
[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。
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 页面也会出现在静态资源缺失时。