
go 的 `http.request.body` 不会自动全量缓冲,服务器在接收到请求头后立即调用 `servehttp`,而请求体按需读取;若不主动读取或限制读取超时,慢速攻击(如 slowloris)可能导致连接长期占用和内存/连接数耗尽。
在 Go 的 net/http 包中,http.Request.Body 是一个实现了 io.ReadCloser 接口的流式读取器(通常是 *io.LimitedReader 包裹的底层网络连接),它本身不预先缓存整个请求体。这意味着:
- ✅ 服务器在解析完 HTTP 请求头(即 GET /path HTTP/1.1 及其 headers)后,立即调用你的 ServeHTTP 方法,无需等待请求体(如 POST 数据)传输完成;
- ❌ r.Body 不是内存中的字节切片或缓冲区,而是对底层 net.Conn 的封装——数据仅在你显式调用 io.ReadAll(r.Body)、r.ParseForm()、json.NewDecoder(r.Body).Decode() 等读操作时,才从 TCP 连接中按需拉取;
- ⚠️ 但这也带来风险:如果你的 handler 完全忽略 r.Body(例如对 POST 请求不做任何读取),该连接将保持打开状态,底层 socket 缓冲区可能持续积压未读数据,而 Go 的 HTTP server 不会主动关闭空闲的请求体读取连接——这正是 Slowloris 类攻击的突破口。
正确应对方式:超时控制 + 主动读取
最直接、有效的防护不是依赖“是否缓冲”,而是通过 连接级与读取级超时 来限制恶意行为:
server := &http.Server{
Addr: ":8080",
Handler: &MyHandler{},
ReadTimeout: 5 * time.Second, // 从连接建立到读完 request header 的总时间上限
ReadHeaderTimeout: 2 * time.Second, // 仅限制读取 header 的时间(Go 1.8+)
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
log.Fatal(server.ListenAndServe())? 关键说明: ReadTimeout 从 Accept 开始计时,覆盖 header 解析 + body 读取全过程; ReadHeaderTimeout 更精准,仅约束 header 解析阶段(防止 header 慢速攻击); 即使 handler 内部未读 r.Body,超时仍会触发连接关闭,避免资源长期悬挂。
此外,在业务逻辑中也应主动、有界地消费请求体,尤其对大文件上传或未知大小的 POST:
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
// 限制最大读取量(例如 10MB),防止 OOM
limitedBody := http.MaxBytesReader(w, r.Body, 10<<20)
data, err := io.ReadAll(limitedBody)
if err != nil {
http.Error(w, "Request body too large or read error", http.StatusRequestEntityTooLarge)
return
}
// 处理 data...
}
}总结
- Go 不自动缓冲整个 r.Body,但也不强制你立即读取——这赋予灵活性,也带来责任;
- 慢速 POST 攻击的风险根源在于无超时的长连接 + 未读 Body,而非“Go 是否缓冲”;
- 生产环境必须配置 ReadTimeout / ReadHeaderTimeout,并结合 http.MaxBytesReader 对请求体设限;
- 切勿假设“不读 Body 就没事”——未读的 Body 会让连接持续挂起,成为 DoS 温床。
遵循以上实践,即可在保持 Go 高并发优势的同时,有效抵御 Slowloris 及类似资源耗尽型攻击。










