Gin中间件默认不支持请求体Gzip解压,因HTTP规范未要求且实际极少使用;需在特定路由handler内手动用gzip.NewReader包装Body,并确保正确实现io.ReadCloser接口,避免提前读取或重复解析。

Gin 中间件本身不提供对请求体(Request.Body)的 Gzip 解压缩支持——它只处理响应压缩。若需解压客户端发来的 Content-Encoding: gzip 请求体(比如上传的压缩 JSON 或表单数据),必须手动包装 http.Request.Body,且时机和顺序非常关键。
为什么 Gin 默认不自动解压请求体
HTTP 规范不要求服务器自动解压请求体;gzip 压缩请求体属于客户端可选行为,实际中极少使用(浏览器 FormData / Fetch 不会默认 gzip POST body,API 客户端也多绕过)。Gin 作为轻量框架,不内置该逻辑是合理取舍。强行在中间件里统一解压,反而会破坏 c.GetRawData()、c.ShouldBindJSON() 等方法的预期行为。
手动解压请求体的正确时机:必须在路由匹配后、业务 handler 执行前
不能在全局中间件(如 r.Use())里提前解压 req.Body,否则后续中间件或 Gin 内部的绑定逻辑可能重复读取、panic 或静默失败。正确做法是在具体需要解压的路由 handler 内部做:
- 先检查
c.Request.Header.Get("Content-Encoding") == "gzip" - 用
gzip.NewReader(c.Request.Body)包装原始 body - 替换
c.Request.Body为新 reader(注意:要实现io.ReadCloser接口,含Close()) - 确保只调用一次
c.ShouldBindJSON()或c.GetRawData(),避免 body 被多次读取
常见错误:body 被提前读取或未重置
以下情况会导致解压失败或 panic:
-
c.PostForm()或c.DefaultPostForm()被调用过——它们会隐式调用ParseMultipartForm,消耗原始 body - 在解压前已执行
c.ShouldBindJSON(&v),此时 body 已空,gzip.NewReader会返回unexpected EOF - 替换
c.Request.Body后没实现Close()方法,导致连接无法复用或内存泄漏 - 未检查
Content-Length,对超大请求不做限制,易被 DoS
最小可行解压示例(仅用于特定接口)
func gzipRequestBody() gin.HandlerFunc {
return func(c *gin.Context) {
enc := c.Request.Header.Get("Content-Encoding")
if enc != "gzip" {
return
}
gz, err := gzip.NewReader(c.Request.Body)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "invalid gzip body"})
return
}
// 必须实现 Close(),否则连接泄漏
c.Request.Body = struct {
io.Reader
io.Closer
}{gz, gz}
}
}
// 使用时仅挂载到需要解压的路由
r.POST("/api/compressed", gzipRequestBody(), func(c *gin.Context) {
var payload map[string]interface{}
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, payload)
})
真正需要解压请求体的场景极少,多数时候是误以为“响应压缩”和“请求解压”对称。如果上游有 Nginx 或 API 网关,更应由它们统一处理请求解压,Gin 层保持干净。一旦决定自己做,务必把解压逻辑收束在明确路径下,而非全局中间件——这是最容易被忽略、也最易引发连锁问题的点。


















