必须显式用 http.MaxBytesReader 包装 r.Body,因为 Echo 的 e.MaxRequestBodySize 仅在路由匹配前生效,一旦中间件或 handler 提前读取 r.Body(如 ParseForm、FormFile),原始 body 即被清空导致限流失效。

必须显式用 http.MaxBytesReader 包装 r.Body,Echo 自带的 e.MaxRequestBodySize 在多数场景下不生效——尤其当你调用了 c.FormFile()、c.MultipartForm() 或任何提前读取 r.Body 的操作之后。
为什么 e.MaxRequestBodySize 经常失效
Echo 的 e.MaxRequestBodySize 底层确实调用了 http.MaxBytesReader,但它只在请求进入路由匹配前生效。一旦中间件或 handler 提前消费了 r.Body(比如解析 multipart、调用 ParseForm()、甚至 io.ReadAll(r.Body)),原始 body 就已为空,后续包装形同虚设。
- 自定义 JWT 中间件里执行了
r.ParseForm()→r.Body被清空,限流失效 - handler 中绕过
c.ShouldBind(),直接用json.NewDecoder(c.Request.Body).Decode()→ 没手动套http.MaxBytesReader,等于裸奔 - 用了
c.FormFile("file")→ 它内部会调用r.ParseMultipartForm(),提前读取并丢弃 body 流
正确做法:在 handler 开头手动限流
对每个需要上传的 endpoint,在 handler 最开头就包装 r.Body,且必须在任何解析逻辑之前执行:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
func uploadHandler(c echo.Context) error {
// ✅ 必须放在最前面!
c.Request().Body = http.MaxBytesReader(c.Response(), c.Request().Body, 10*1024*1024) // 10MB
<pre class="brush:php;toolbar:false;">// ❌ 不能再调 c.FormFile() 或 c.MultipartForm()
file, err := c.FormFile("video")
if err != nil {
return c.String(http.StatusBadRequest, "no file received")
}
src, err := file.Open()
// ...}
- 限流值单位是字节,
10*1024*1024表示 10MB;不要写成10MB字面量(Go 不支持) - 若需支持大文件分片上传,必须设
c.Request().ParseMultipartForm(0)禁用自动解析,否则c.FormFile()会吞掉原始 body - 若同时处理 multipart 表单字段 + 文件,得用
http.MaxBytesReader限制总大小,再用r.ParseMultipartForm(8 * 1024 * 1024)控制内存缓存上限(如 8MB),超出部分自动落盘
multipart 场景下只限总大小还不够
攻击者可构造一个 1KB 文本字段 + 99MB 文件,轻松绕过 “总大小 10MB” 的限制——因为 r.ParseMultipartForm(maxMemory) 的 maxMemory 参数只管内存缓冲区,不拦总上传量。
- 先用
http.MaxBytesReader卡死总上传量(如 50MB) - 再调
r.ParseMultipartForm(8 * 1024 * 1024),让小文件走内存、大文件落磁盘 - 最后用
r.FormFile("file")获取磁盘句柄,而非r.FormValue()(后者只读内存中已解析的部分) - 注意:校验文件扩展名或 MIME 类型不能只靠后缀,需用
finfo_file()检查二进制头
真正难的不是加一行限流代码,而是确保它出现在所有可能读取 r.Body 的操作之前——包括你没意识到的中间件、日志记录器、或第三方插件。只要有一处提前读取,整个防护就塌了。










