Go的http响应数据写入顺序错误,造成实际响应头与预期不一致问题
简单说下背景
在gin中,封装了一个代理请求,将传入的请求参数封装为新请求并请求第三方,将响应重新包装并返回;
再封装响应时,先调用了c.Writer.Write(resBody),然后操作Header,后加的header实际响应中缺失
package mainimport ("io""net/http""github.com/gin-gonic/gin"
)func main() {g := gin.Default()g.Any("/proxy1/*proxyPath", func(c *gin.Context) {proxyReq, err := http.NewRequest(c.Request.Method,"https://test.cn:9443/api/v1/Download",c.Request.Body)res, err := http.DefaultClient.Do(proxyReq)if err != nil {c.Error(err)return}defer func(Body io.ReadCloser) { _ = Body.Close() }(res.Body)c.Writer.WriteHeader(res.StatusCode)var resBody []byteresBody, err = io.ReadAll(res.Body)_, err = c.Writer.Write(resBody) // 先Writeif err != nil {c.Error(err)return}for k, v := range res.Header {c.Header(k, v[0]) // 再操作了 Header}return})g.Run(":8080")
}
排查过程省略了,直接说原因
标准库net/http的ResponseWriter写入方法调用有顺序要求:
- 在执行Write时,如果没有WriteHeader,会先执行WriteHeader,且WriteHeader只会执行一次
标准库和gin的ResponseWriter
接口定义
// 标准库
type ResponseWriter interface {Header() HeaderWrite([]byte) (int, error)WriteHeader(statusCode int)
}// gin ResponseWriter 包装了标准库的ResponseWriter
type ResponseWriter interface {http.ResponseWriterhttp.Hijackerhttp.Flusherhttp.CloseNotifier// Status returns the HTTP response status code of the current request.Status() int// Size returns the number of bytes already written into the response http body.// See Written()Size() int// WriteString writes the string into the response body.WriteString(string) (int, error)// Written returns true if the response body was already written.Written() bool// WriteHeaderNow forces to write the http header (status code + headers).WriteHeaderNow()// Pusher get the http.Pusher for server pushPusher() http.Pusher
}
- gin的c.Writer.WriteHeader(res.StatusCode) 实际不会调用标准库的WriteHeader,而是等到Write(resBody)时再调WriteHeaderNow
// github.com/gin-gonic/gin@v1.10.1/response_writer.go:64
func (w *responseWriter) WriteHeader(code int) {if code > 0 && w.status != code {if w.Written() {debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)return}w.status = code}
}
// github.com/gin-gonic/gin@v1.10.1/response_writer.go:74
func (w *responseWriter) WriteHeaderNow() {if !w.Written() {w.size = 0w.ResponseWriter.WriteHeader(w.status)}
}
代码
package mainimport ("net/http"
)func main() {http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// 会更新到实际响应w.Header().Add("C1111", "会更新到实际响应")// 写入数据 会自动WriteHeader且code为200_, err := w.Write([]byte("hello world"))_, err = w.Write([]byte("hello world11"))// 不生效了w.Header().Add("C2222", "不会添加到最终的响应头中")// 触发:http: superfluous response.WriteHeader call from main.main.func1 (net_http_server.go:23)// 警告:已经执行过 WriteHeader// 此时请求返回依然是500w.WriteHeader(500)if err != nil {panic(err)}})_ = http.ListenAndServe(":7878", nil)
}
总结
- 先
header()
:Header().Add(k,v), w.Header().Del()等 - 再
WriteHeader(statusCode)
:- 标准库WriteHeader(statusCode)会写入所有header,且后面无法再写入
- gin框架的ResponseWriter包装了标准库的ResponseWriter,WriteHeader只会记录一下status,WriteHeaderNow()才会实际调用标准库的WriteHeader(statusCode)
- 再
Write([]byte)
:- 如果没有WriteHeader,会自动WriteHeader且code为200,Write后无法再返回其他状态码:404, 500等;
go1.24/src/net/http/server.go:1687
- 调用write后,再读取
请求体
可能报错; - Write可以调多次
- 如果没有WriteHeader,会自动WriteHeader且code为200,Write后无法再返回其他状态码:404, 500等;