“我在 handler 最后加了个响应头,客户端怎么没收到?”这篇用真实的 HTTP 客户端把规则列清楚:什么时候设置才生效、不设 Content-Type 会被嗅探成什么、在 net/http 层重复 WriteHeader 会发生什么,以及真正需要在正文之后发送的信息该怎么办。
/before → 200 客户端看到的 X- 头:[X-Step=2-before-again]
/after-body → 200 客户端看到的 X- 头:[]
/after-status → 200 客户端看到的 X- 头:[]
/status-only → 202 客户端看到的 X- 头:[X-Late=after-c.Status(没写出)]
/delete → 200 客户端看到的 X- 头:[]响应头要在状态行和第一段正文发出之前设置。之前设多次,以最后一次为准;c.Header(key, "") 等于删除。写出之后(c.String 之后、WriteHeaderNow 之后)再设,只是改了内存里的 map,客户端收不到。注意只调用了 c.Status 而没写正文时头还没发出,这时设置仍然有效——判断依据是 c.Writer.Written()。
/sniff-html Content-Type="text/html; charset=utf-8"
/sniff-text Content-Type="text/plain; charset=utf-8"
/sniff-json Content-Type="text/plain; charset=utf-8"
/sniff-png Content-Type="image/png"
/set-before Content-Type="application/json"
/set-after Content-Type="text/html; charset=utf-8"
/c-string Content-Type="text/plain; charset=utf-8"没有显式设置 Content-Type 时,net/http 在第一次写时根据前 512 字节嗅探:HTML 是 text/html,PNG 是 image/png,JSON 文本只是 text/plain。写完之后再设已经来不及。用 c.JSON / c.String 这些方法时 Gin 已经替你设好了,只有直接写 c.Writer 时才需要注意。
/gin-twice → 客户端状态 201;服务器 ErrorLog:(没有日志)
/raw-twice → 客户端状态 201;服务器 ErrorLog:http: superfluous response.WriteHeader call from main.main.func2 (main.go:8)
http: superfluous response.WriteHeader call from github.com/gin-gonic/gin.(*responseWriter).WriteHeaderNow (response_writer.go:80)Gin 自己的 WriteHeader 只是记录状态,所以在 Gin 层写多次既安全又没有日志,第一次发出的状态生效。如果拿到底层 http.ResponseWriter 直接写,标准库会把 http: superfluous response.WriteHeader call 记到服务器的 ErrorLog 里;而且因为 Gin 不知道你已经写过,请求结束时它又替你写了一次,所以会有两条日志。
读 body 之前:resp.Trailer=map[],TransferEncoding=[chunked]
读完 body 之后:正文="hello trailer world" resp.Trailer=map[X-Body-Sha256:[b4dc653fd8bab335]];X-Not-Trailer=""真要在正文之后附带信息(比如边写边算的校验和),HTTP 的办法是 trailer:在写完正文之后用 http.TrailerPrefix 作为前缀设置头,服务器会用 chunked 编码把它们放在最后,客户端要读完 body 才能从 resp.Trailer 取到。很多客户端和中间件不处理 trailer,本文只验证了 Go 的客户端。