第 8 / 23 篇

第7章 HTTP 输入边界与慢上传

请求头和请求体是不同阶段

一个连接建立以后,可以很久都不发送完整请求头,也可以快速发送请求头后极慢上传请求体。ReadHeaderTimeout 主要限制头部读取,不能替你定义所有请求体的可接受速度。全局 ReadTimeout 又可能把大文件上传和普通小请求放在同一种时间限制下。应先区分端点的输入协议,再选择对应限制。

随书 /tasks 使用经过校验的查询参数构造任务,不接受任意大 JSON 作为队列元素。这让实验聚焦生命周期,也降低了输入体积的影响。但真实业务如果读取 JSON,仍需先限制 Body,再限制解码结构和字段。只设置 Content-Length 上限不够,因为客户端可以不发送该头,或使用其他传输方式。

http.MaxBytesReader 可限制处理函数读取的字节数;解码失败应及时结束,不要把超限输入继续完整读入内存。限制还应覆盖解压后的体积、字符串长度、数组项数量等业务维度。一个压缩后很小的请求,解压后仍可能占据大量内存。协议字节预算和业务对象预算互相补充。

不信任可以无限增长的表示

HTTP header 的大小、URL 长度、路径段、表单值和错误消息都有内存成本。标准库会提供一些协议层保护,但业务仍需限制自己保留的数据。尤其不要为了日志把整个 Body 复制两份,再把它保存进 context 或后台任务。诊断信息应保留长度、摘要和必要字段,而不是默认保存全部输入。

参数解析也要有语义边界。延迟参数不能为负,也不能大到让任务长时间占槽;字节数不能溢出或超过演示允许范围;外呼地址必须说明教学场景和部署限制。随书外呼能力用于本地故障实验,若放到不可信公网,就需要目标地址、DNS 与网络出口策略,不能把它直接当成通用代理。

校验失败应在接纳之前发生。一个格式错误的任务若先占入队槽,再交给 worker 判断,会让便宜的无效输入消耗宝贵执行资源。与此同时,接纳前校验也应保持有界,不能为了验证请求而同步执行长时间外呼。把校验和业务执行混在一起,会破坏“满即拒绝”的廉价路径。

请求取消与读取进度

服务器请求 context 会在客户端连接关闭、请求取消或处理函数结束等条件下取消,但协议版本与代理行为会影响具体传播路径。测试应使用真实 TCP 或真实 HTTP 客户端,不应只手工 cancel 一个 context 就宣称已经覆盖网络断开。手工取消单元测试检查的是函数合作;网络测试检查的是协议连接到函数的链路。

对于慢上传,处理函数可能阻塞在 Body.Read。要让它最终退出,需要适当读取截止或连接生命周期约束。仅在 Read 之前检查一次 ctx.Err 不够,因为随后可能进入长时间阻塞。ResponseController 提供按响应控制器设置读取截止的能力,但包装器需要支持相关接口;使用中间件时应验证实际路径。

不要为了感知断连持续启动额外 goroutine 轮询。标准库已经管理请求取消,业务只需把信号传下去。多加一层侦听者不仅增加资源,还会引入它自己的退出责任。每个监控 goroutine 都应有清楚终点,否则诊断代码本身也会制造泄漏。

错误响应也遵守写入规则

头部写出之后就不能再更改状态码。流式端点尤其容易先返回一部分成功内容,随后才发现上游错误;这时应使用协议内错误帧或关闭连接,不能假装还能重新发送 HTTP 错误状态。普通 JSON 接口若希望统一错误结构,可以在有限大小缓冲区完成结果后一次写出,但缓冲也需要预算。

客户端中断时写错误响应常常也会失败。处理函数应记录必要信息并结束,避免为了“必须告知客户端”进入反复重试。一次已无法交付的响应不值得继续占住 worker。写入错误需要参与控制流,而不是被 _ = 一概忽略。

请求 Body 的使用者也应遵守生命周期。标准 HTTP Server 通常负责处理请求体关闭,但业务若把它交给后台继续读取,就越过了处理函数范围。真正异步的上传处理应先把数据存入受控持久介质,接纳成功后只传引用。内存队列里的任务描述应该独立于即将结束的 HTTP 请求。

练习:用很慢的客户端做输入验收

通过 net.Dial 手工发送不完整请求头,确认头部读取期限最终关闭连接。再发送完整头部但缓慢写 Body,观察它受到哪一种限制。两次实验都记录实际等待上限,不要只检查服务是否返回错误。使用独立测试端口,避免把故障流量发向真实服务。

把输入解析限制与工作池容量测试串联:超大或非法输入不应增加 accepted;合法输入在队列满时应及时拒绝;解除阻塞后仍可正常提交。这样可以证明输入路径没有绕过接纳边界,也不会因某个失败请求留下持续占用。

本章实测证据

负延迟、非HTTP目标和过大的流字节数被输入校验拒绝。慢上传是本章扩展练习,不属于这个测试已覆盖的结果。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。

--- PASS: TestHTTPInvalidInput (0.00s)