必须在ParseForm或ReadAll前调用http.MaxBytesReader包装r.Body,否则限制失效;需显式捕获http.ErrBodyTooLarge并返回413;multipart场景还需配合ParseMultipartForm和Size校验。

必须在 ParseForm 或 ReadAll 前调用 http.MaxBytesReader
不提前包装 r.Body,限制就等于没设。Go 的 http.MaxBytesReader 不是拦截器,它只在你真正调用 r.Body.Read()(包括 r.ParseForm()、json.NewDecoder(r.Body).Decode()、io.ReadAll(r.Body))时才开始计数并拦截。一旦这些操作先于包装发生,Body 可能已被部分甚至全部读进内存,后续再套 MaxBytesReader 就毫无意义。
常见错误现象:panic: runtime: out of memory、服务响应变慢但日志无报错、http: TLS handshake error 伴随连接堆积——这些往往就是 Body 已悄悄被读取导致的。
正确顺序只能是:
- 第一行 handler 代码:赋值
r.Body = http.MaxBytesReader(w, r.Body, limit) - 之后才能调用
r.ParseForm()、r.FormValue()、json.Decode()等任何触发读取的操作 - 如果用了中间件(如 JWT 验证),确保它不调用
io.ReadAll(r.Body)或r.ParseForm()——否则限制在进入业务 handler 前就已失效
http.MaxBytesReader 和 http.MaxBytesHandler 的区别
http.MaxBytesReader 是 per-handler 控制,需要你在每个 handler 开头手动包装 r.Body;而 http.MaxBytesHandler 是全局 wrapper,作用于整个 http.Handler 链,在请求进入任何路由前就完成限制,更安全、不易遗漏。
如果你用的是 http.ServeMux 或自定义 http.Handler,推荐直接包一层:
mux := http.NewServeMux()
mux.HandleFunc("/api/login", loginHandler)
mux.HandleFunc("/api/upload", uploadHandler)
log.Fatal(http.ListenAndServe(":8080", http.MaxBytesHandler(mux, 10<p>注意:<code>http.MaxBytesHandler</code> 无法区分不同路由的语义需求(比如登录接口只需 2MB,上传接口要 50MB),这种场景仍得回到 per-handler 的 <code>MaxBytesReader</code>。</p><p>另外,框架如 Gin 的 <code>c.Request.Body</code> 本质也是封装后的流,但如果你绕过 <code>c.ShouldBindJSON()</code> 直接读 <code>c.Request.Body</code>,依然要自己套 <code>MaxBytesReader</code>,否则框架的限制不生效。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig"><img
src="https://img.php.cn/upload/skill/000/000/081/179215391842073.jpg" alt="Golang Uber Dig" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="overflowclass">Golang Uber Dig</a>
<p class="overflowclass">使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等</p>
</div>
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><h3>multipart/form-data 场景下不能只靠 MaxBytesReader</h3><p>单靠 <code>http.MaxBytesReader</code> 限制总字节数,对 multipart 请求只是第一道防线。它无法阻止攻击者构造一个“小表单 + 超大文件”的组合:比如总大小卡在 10MB 内,但其中 9.9MB 是恶意文件,而 <code>ParseMultipartForm</code> 默认会把整个文件加载进内存(除非你设了 <code>maxMemory</code>)。</p><p>所以必须配合:</p>
- 显式调用
r.ParseMultipartForm(maxMemory),例如32 (32MB),让 Go 把超限部分自动写入磁盘临时文件 - 调用
r.FormFile("file")后,立刻检查header.Size,因为FileHeader.Size是解析出的真实字节数(不是Content-Length头,后者可伪造) - 不要依赖
r.ContentLength判断大小——HTTP/1.1 chunked 编码下它为-1,完全不可信
错误处理必须显式判断 http.ErrBodyTooLarge
http.MaxBytesReader 触发超限时,不会 panic,也不会自动返回 413。它会让后续的 Read() 操作返回 http.ErrBodyTooLarge 错误,而这个错误**必须由你显式捕获并响应**,否则可能静默失败或返回 500。
典型漏点:
- 用
json.NewDecoder(r.Body).Decode(&v)却没检查 error 类型 - 在
ParseForm()后只检查err != nil,没用errors.Is(err, http.ErrBodyTooLarge) - 以为
http.Error(w, ..., http.StatusRequestEntityTooLarge)能自动触发,其实它只是写响应,不终止 handler 执行
推荐写法:
if err := r.ParseForm(); err != nil {
if errors.Is(err, http.ErrBodyTooLarge) {
http.Error(w, "request entity too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "bad request", http.StatusBadRequest)
return
}
真正容易被忽略的是:这个错误类型只在你用标准库方式读取 Body 时才出现;如果用了第三方 JSON 库或自定义解析逻辑,且没透传底层 Read() 错误,ErrBodyTooLarge 就会丢失。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










