go 的 http.server 默认不限制上传体大小,易导致内存耗尽;需在 handler 开头用 r.contentlength 判断或配合 http.maxbytesreader 限制读取上限。

Go 的 http.Server 默认不限制上传体大小,但会吃光内存
Go 标准库的 http.Server 对请求体(包括文件上传)不做任何大小限制。一旦遇到恶意构造的大文件 POST 请求(比如 1GB 的 multipart/form-data),ParseMultipartForm 会把整个内容读进内存或临时磁盘,不设防就等于给 DoS 留了后门。
实操建议:
-
http.Server.MaxHeaderBytes只管 header,不管 body,别指望它拦上传 - 必须在
http.Request被解析前就做拦截——也就是在Handler开头用r.ContentLength做快速拒绝 -
r.ContentLength在分块传输(chunked encoding)时为 -1,此时只能靠io.LimitReader配合http.MaxBytesReader控制读取上限 - 示例:在 handler 开头加
r.Body = http.MaxBytesReader(w, r.Body, 10(限 10MB),超限直接返回 <code>413 Payload Too Large
ParseMultipartForm 的 maxMemory 参数不是总大小限制
很多人以为调用 r.ParseMultipartForm(32 就能限制整个上传不超过 32MB,其实它只控制「内存中缓存的 form data 大小」,超出部分自动落盘到 <code>os.TempDir(),总上传量仍不受控。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 上传 500MB 文件没报错,但服务内存没爆、磁盘却被写满
- 临时文件残留(尤其 panic 或提前 return 时),
os.RemoveAll(os.TempDir())别乱跑 -
maxMemory = 0不代表禁用内存缓存,而是退化为全落盘,反而更慢且更难限流
正确做法:
- 先用
http.MaxBytesReader封装r.Body,再调ParseMultipartForm -
maxMemory设为合理值(如 32MB),避免小文件也频繁落盘;真正要限的是总上传量,不是内存用量
客户端断连时 http.MaxBytesReader 不会自动 cleanup
http.MaxBytesReader 在读取超限时会返回 http.ErrBodyReadAfterClose 或 io.EOF,但它不会主动关闭底层连接或清理已读的临时数据。如果客户端上传到一半断开(比如网络抖动或主动 cancel),Go 的 net/http 默认不会立刻感知,可能让 goroutine 卡在 Read 上,积压连接。
关键细节:
-
http.Server.ReadTimeout和WriteTimeout已被弃用,应改用ReadHeaderTimeout+IdleTimeout - 对上传接口,建议单独设置更短的
IdleTimeout(如 30s),并配合context.WithTimeout在 handler 内控制整体生命周期 - 临时文件不会因 context cancel 自动删除,需手动调用
multipart.File.Close()或清空form.RemoveAll()
生产环境必须区分「上传路径」和「通用路由」做独立限流
一个 http.Server 通常同时处理 API、静态资源和文件上传。如果所有请求共用同一套超时和读取限制,API 接口可能因上传配置过严而误伤(比如 10s 超时对大文件显然不够),上传又可能因 API 配置过松而失控。
推荐结构:
- 用
http.ServeMux或第三方路由(如chi)为/upload/*单独挂载 handler - 该 handler 内部使用独立的
context.WithTimeout、http.MaxBytesReader和ParseMultipartForm参数 - 避免在 middleware 层统一 wrap
r.Body,否则非上传路由也会承受不必要的封装开销 - 日志里记录
r.ContentLength和实际读取字节数,便于事后识别扫描或攻击流量
最易被忽略的一点:限流逻辑必须放在 Request.Body 第一次被读之前。一旦有其他中间件(比如 JWT 解析、gzip 解包)提前调用了 r.Body.Read,再 wrap 就晚了。










