Gin 的 e.MaxRequestBodySize 有时不生效,是因为它依赖 http.MaxBytesReader 在请求体首次读取前包装 r.Body;若中间件或 handler 提前调用 ParseForm、ReadAll 等导致 Body 被消费,则限流失效。

为什么 Gin 的 e.MaxRequestBodySize 有时不生效
它本质是调用 http.MaxBytesReader,但只在请求进入框架处理链的最早入口处包装 r.Body。一旦中间件或业务逻辑提前读取了 Body(比如 JWT 中间件调用了 r.ParseForm() 或 io.ReadAll(r.Body)),原始 Body 就已被消费,后续框架的包装就等于没做。
常见失效场景包括:
- 自定义中间件中调用了
r.ParseForm()、r.FormValue()或io.ReadAll(r.Body) - handler 中绕过
c.ShouldBindJSON(),直接用json.NewDecoder(c.Request.Body).Decode()却没手动套http.MaxBytesReader - 使用了自定义
gin.Engine实例,但未确认gin.DefaultWriter是否启用限流(v1.9+ 默认启用,但手动构造时可能被覆盖)
http.MaxBytesReader 必须在任何 Body 读取前调用
它不是“设个开关就拦截”的中间件,而是一个带计数的 Reader 包装器——只有第一次 r.Body.Read()(含 r.ParseForm()、io.ReadAll()、json.Decode() 等)才会触发检查。如果 Body 已被提前读空,再包装也无济于事。
正确写法必须是 handler 第一行就赋值:
立即学习“go语言免费学习笔记(深入)”;
func myHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10MB
// 后续才能调用 ParseForm / ShouldBind / FormFile 等
if err := r.ParseForm(); err != nil {
if errors.Is(err, http.ErrBodyTooLarge) {
http.Error(w, "Request Entity Too Large", http.StatusRequestEntityTooLarge)
return
}
// ...
}
}
容易踩的坑:
- 忘记把包装后的 Body 赋值回
r.Body - 把
w传错成r(第二个参数必须是http.ResponseWriter) - 在调用
http.MaxBytesReader前已执行过任何读取操作
multipart 场景下仅靠 MaxBytesReader 不够
r.ParseMultipartForm(maxMemory) 的 maxMemory 参数只控制内存缓存部分,超出会落盘——它不限制总上传体积。攻击者可发一个 1KB 表单 + 99MB 文件,只要总大小没超限,MaxBytesReader 就不会拦截,而 ParseMultipartForm(32 仍会让大文件写入临时目录。
真正安全的组合是:
- 先用
r.Body = http.MaxBytesReader(w, r.Body, 50 限制总请求体(如 50MB) - 再调
r.ParseMultipartForm(8 ,设较小的 <code>maxMemory(如 8MB),让小字段走内存、大文件落盘 - 最后用
r.FormFile("file")获取磁盘句柄,避免r.FormValue()触发全量读取
全局限流选 http.MaxBytesHandler,按路由区分用 MaxBytesReader
http.MaxBytesHandler 包裹整个 http.Handler,在路由匹配前就拦截超限请求,返回 413 并关闭连接,最安全、不易遗漏。但它无法区分语义:登录接口和上传接口被迫共用同一上限。
示例:
mux := http.NewServeMux()
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/upload", uploadHandler)
log.Fatal(http.ListenAndServe(":8080", http.MaxBytesHandler(mux, 10<<20))) // 全局 10MB
若需差异化限制(如登录 2MB、上传 50MB),必须回到 per-handler 的 http.MaxBytesReader,并在每个 handler 开头显式包装。
复杂点在于 multipart + 自定义中间件 + 多层绑定的组合场景——Body 可能被多个地方悄悄读取,得逐层检查是否提前消费;最容易被忽略的是中间件里那行看似无害的 r.ParseForm()。


















