直接用 gzip.NewReader 读 c.Request.Body 会失败,因为 Gin 的 Body 是一次性 io.ReadCloser,已被中间件或 ShouldBindJSON 等方法提前读取耗尽,导致 EOF 或无效头;必须在开头用 io.ReadAll 配合 magic 字节和 Content-Encoding 校验,并用 http.MaxBytesReader 限流后解压。

为什么直接用 gzip.Reader 读 *http.Request.Body 会失败?
因为 Gin 的 c.Request.Body 默认已被中间件(比如 gin.Recovery() 或自定义日志)提前读取过,或者被 c.ShouldBindJSON() 等方法消费,导致底层 io.ReadCloser 已 EOF 或缓冲区为空。直接套 gzip.NewReader(c.Request.Body) 会报 gzip: invalid header 或空内容。
- 检查是否已调用过
c.PostForm()、c.GetRawData()、c.ShouldBind()—— 这些都会消耗 Body - Gin 默认不缓存 Body,无法重复读取;必须在最开始就接管原始流
- 若前端发的是
Content-Encoding: gzip,需手动解压,Gin 不自动处理
如何安全获取原始 gzip 请求体并解压?
必须绕过 Gin 默认的 Body 消费逻辑,在中间件或 handler 开头立刻处理:
- 用
io.ReadAll(c.Request.Body)先完整读取原始字节(注意内存风险,见下一条) - 检查
c.Request.Header.Get("Content-Encoding") == "gzip" - 用
gzip.NewReader(bytes.NewReader(rawBytes))创建解压 reader - 再用
io.ReadAll()读解压后内容,或直接传给 JSON 解析器
示例关键片段:
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "read body failed"})
return
}
if c.Request.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid gzip"})
return
}
defer gr.Close()
raw, err = io.ReadAll(gr)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "decompress failed"})
return
}
}
// 此时 raw 是解压后的明文 []byte
var data MyStruct
if err := json.Unmarshal(raw, &data); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "json parse failed"})
return
}
大字段场景下怎么避免 OOM?
一次性 io.ReadAll 整个请求体对大文本(如 >10MB)极危险,Go 默认无流式限流,容易触发内存暴涨甚至被系统 kill。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
http.MaxBytesReader包装原始c.Request.Body,限制最大读取量(例如 50MB) - 解压时改用流式处理:不全读进内存,而是用
gzip.NewReader包装后直接喂给json.NewDecoder - 确保
gzip.NewReader的输入 reader 是带限流的,而非原始 Body
推荐做法(流式 + 限流):
limitedBody := http.MaxBytesReader(c.Writer, c.Request.Body, 50*1024*1024) // 50MB
var reader io.ReadCloser = limitedBody
if c.Request.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(limitedBody)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "gzip header error"})
return
}
defer gr.Close()
reader = gr
}
decoder := json.NewDecoder(reader)
var data MyStruct
if err := decoder.Decode(&data); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "stream decode failed"})
return
}
哪些地方容易漏掉 gzip header 校验?
很多人只检查 Content-Encoding,但实际还应校验 Content-Type 和 gzip magic bytes,否则可能把非 gzip 数据误当 gzip 解压,导致 panic 或静默错误。
- 前端若没设
Content-Encoding: gzip,即使数据是 gzip 压缩的,服务端也不该解压 - 更稳妥的做法:先 peek 前 2 字节,判断是否为
0x1f 0x8b(gzip magic) - 若 header 和 magic 不一致,按明文处理,避免强制解压失败
- Gin 日志中间件默认会读 Body,所以必须把它禁用或移到解压之后
peek 示例:
buf := make([]byte, 2)
n, _ := c.Request.Body.Read(buf)
if n == 2 && buf[0] == 0x1f && buf[1] == 0x8b && c.Request.Header.Get("Content-Encoding") == "gzip" {
// 确认是 gzip,重置 reader 并解压
c.Request.Body = io.NopCloser(io.MultiReader(bytes.NewReader(buf), c.Request.Body))
}
真正上线时,magic byte 校验 + header 校验 + MaxBytesReader 三者缺一不可。gzip 解压本身不耗 CPU,但错误输入会导致 panic 或无限循环,而大字段下内存失控比 CPU 更致命。

















