bigywater.tech
Gin · 29

WriteHeader 之后再改 header 会怎样

“我在 handler 最后加了个响应头,客户端怎么没收到?”这篇用真实的 HTTP 客户端把规则列清楚:什么时候设置才生效、不设 Content-Type 会被嗅探成什么、在 net/http 层重复 WriteHeader 会发生什么,以及真正需要在正文之后发送的信息该怎么办。

0 个
写出之后设置的普通响应头,客户端能看到的个数
text/plain
不设 Content-Type 时 {"a":1} 被 Go 嗅探出的类型(不是 application/json)
2 条
绕过 Gin 在底层写两次 WriteHeader,服务器 ErrorLog 里的 superfluous 日志数(Gin 这层是 0 条)

什么时候设置才生效

实测 · 本机执行输出
    /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()。

Content-Type 的嗅探

实测 · 本机执行输出
    /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 时才需要注意。

重复 WriteHeader

实测 · 本机执行输出
    /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 不知道你已经写过,请求结束时它又替你写了一次,所以会有两条日志。

Trailer

实测 · 本机执行输出
    读 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 的客户端。

分步演示

写出之后改头
本步的对象

    本文用到的工具与边界

    • 本机 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.NewServer 与 Go 的 http.Client;第三节给服务器配置了自己的 ErrorLog 来接收日志;每种情形各一次
    • 没有验证:浏览器、curl、反向代理对 trailer 的处理;HTTP/2 下“写出之后设头”的行为;100/103 等 1xx 响应的发送;Content-Length 与 chunked 的选择规则;在 gzip 中间件之后嗅探的行为
    京ICP备2026067502号-1京公网安备11010502063487号