线上服务偶尔冒出一些查不到来源的400错误,客户端坚称自己没发任何异常请求,这种情况十有八九和畸形请求有关。HTTP协议本身允许服务器对无法解析的请求做出自己的处理决定,而Go的net/http包在这件事上有一套相当明确但文档里说得不太多的规则。理解这套规则,对排查问题和加固服务都有实际帮助。

请求行解析阶段的限制
HTTP请求的第一行是请求行,格式为METHOD SP Request-URI SP HTTP-Version CRLF。Go在net/http/internal/chunked和textproto包的配合下逐字节读取这一行。如果请求行无法匹配任何已知的方法名,或者URI中包含非法字符,服务器会直接返回400 Bad Request并关闭连接。
值得注意的一个细节是,Go对请求行中的空白符非常敏感。有些客户端会在请求行末尾多加一个空格,RFC 7230明确说这是畸形请求,服务器应当拒绝。Go的实现遵循了这一规定,但会做一个容错:如果请求行以额外的空格开头,会尝试跳过。这种宽松和严格并存的策略在源码注释里有说明,目的是兼容一些老旧的浏览器实现。
另一个常见的坑是HTTP版本号。如果客户端发送HTTP/1.2或者HTTP/2.0这样不被支持的版本(在HTTP/1.1的文本通道上),Go会返回400。而发送HTTP/0.9风格的裸请求(只有GET和路径,没有版本号),新版Go已经不再支持,同样以错误处理。
请求头大小与数量限制
这是畸形请求处理中最重要的限制,也是面试和线上问题的高频考点。Go的HTTP服务器默认使用DefaultMaxHeaderBytes,值为1MB。这个值限制的是请求行加上所有请求头的原始字节数,而不是解码后的Header值长度。
当请求头超过这个限制时,Go返回431 Request Header Fields Too Large。下面这段代码演示了如何查看和调整这个限制:
srv := &http.Server{
Addr: ":8080",
MaxHeaderBytes: 1 << 20, // 1MB,默认值
// 设为0则使用系统默认的1MB
// 想更严格可以设小,比如 16KB
}
http.ListenAndServe("", nil) // 这种简便方式用的是默认1MB
注意MaxHeaderBytes只对服务端生效,客户端请求不受此字段控制。另外,如果服务器是通过反向代理转发的,过大的头可能在代理层就被拦下,你需要在Nginx和Go两边都检查。除了总大小,单个头行的长度也有上限约束,超过限制同样触发400或431,错误信息里会出现header too long字样。
头数量本身没有显式的独立限制,但由于总大小限制的存在,实际能容纳的头数量被间接约束了。这一点和某些服务器(比如Apache的LimitRequestFields指令)不同,做迁移时容易产生认知偏差。
请求体阶段的异常处理
请求行和请求头都合法之后,进入请求体处理阶段。这里的畸形情况主要有两类:Content-Length不匹配和Transfer-Encoding异常。
如果请求声明的Content-Length比实际发送的字节数大,客户端又迟迟不发完数据,服务端的Read会一直阻塞,直到超时或连接断开。这就是慢速攻击的原理。Go本身不会主动替你设置读超时,必须手动配置:
srv := &http.Server{
ReadTimeout: 10 * time.Second, // 整个请求读取超时
ReadHeaderTimeout: 5 * time.Second, // 仅读头超时
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
其中ReadHeaderTimeout是1.10之后加入的,专门解决只设置了ReadTimeout时对长连接不友好的问题。四个超时参数各有分工,理解它们的覆盖范围是用好Go HTTP服务器的关键。
另一类是Transfer-Encoding和Content-Length同时出现。按照RFC的规定,这是安全风险,Go会优先采用Transfer-Encoding并忽略Content-Length,但在chunked编码的最后一个chunk格式非法时,读取会返回错误。在你的Handler中,这类错误体现为对request.Body调用Read时返回非nil的error,需要显式处理而不是忽略:
func handler(w http.ResponseWriter, r *http.Request) {
buf := make([]byte, 4096)
for {
n, err := r.Body.Read(buf)
if err != nil {
if err == io.EOF {
break
}
// 畸形请求体,记录日志并中止处理
http.Error(w, "bad request body", http.StatusBadRequest)
return
}
_ = n
}
defer r.Body.Close()
}
防御策略与配置建议
把上面的限制整理一下,可以给出一份实践清单。首先,永远显式创建http.Server并设置全部四个超时参数,不要使用http.ListenAndServe这种无超时的简便方式暴露到公网。其次,根据业务实际需要收紧MaxHeaderBytes,普通API服务16KB到64KB通常绰绰有余。
如果是面向不可信流量的服务,建议在最外层再加一道防护,比如用http.TimeoutHandler给单个Handler兜底,限制整体执行时间:
wrapped := http.TimeoutHandler(apiHandler, 15*time.Second, "timeout")
mux.Handle("/api", wrapped)
最后要理解一点:Go服务器遇到畸形请求时,绝大多数情况下会在进入你的Handler之前就返回4xx错误并关闭连接,你的业务代码感知不到这些请求。所以不要指望在Handler里拦截畸形请求,合适的做法是开启Server.ErrorLog或在接入层记录,监控400和431的突增,那往往意味着扫描器或者配置错误的客户端正在敲门。
Go HTTP服务器畸形请求net/http修改时间:2026-09-16 18:02:42