DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
OnlineSIM:在线接收短信,虚拟号码租用

Hyper HTTP/1 连接已失败,请求 body 为什么还没 Drop

Hyper 1.10.1 的发送与关闭竞争可能让剩余 sender 继续持有 body。显式传递 connection 终止状态,避免只靠 Drop 判断请求失败。

Hyper 的底层 HTTP/1 客户端已经报告连接错误,请求 future 也被丢弃,但请求 body 的 Drop 迟迟没有执行。上层如果等着 body 释放来判断“请求已经失败”,就可能一直等到超时。

Hyper 1.10.1 有过这个具体竞争:请求正在进入内部通道,连接接收端同时关闭;关闭过程在消息真正发布前观察到空队列,剩余 SendRequest 又保留着通道,于是 body 可能直到 sender 被释放才销毁。它不是 Rust 忘记调用 Drop,而是仍有对象持有请求。

handshake 返回不等于协议已经验证成功

低层 hyper::client::conn::http1::handshake 创建 sender 和 connection。实际 I/O 还要靠持续 poll connection 完成。对端发出不符合 HTTP/1 的数据,错误可能在之后驱动 connection 时才出现。

若应用只把 connection 的错误写进日志,同时让另一个任务等待 body 的 Drop 通知,两边就没有可靠的完成关系。调度压力会让这个缺口更容易出现;单次本地请求没有超时,不能证明生命周期设计正确。

先记录三个独立事件:connection future 的结束、send_request future 的结束,以及 body 的释放。不要把其中任意一个时间直接当作其他两个的保证,也不要只看网络连接已经关闭。

把连接终止显式传给请求管理层

应用已经自行管理低层 connection 时,应保留驱动任务的结束结果,让请求管理层据此结束相应的未完成请求,而不是只依赖 Drop 回调。

下面是单一观察者的示意片段,connection 是握手后得到、满足 Tokio spawn 所需 Send 和生命周期条件的 connection future:

let (connection_done_tx, connection_done_rx) = tokio::sync::oneshot::channel();

tokio::spawn(async move {
    let result = connection.await;
    let _ = connection_done_tx.send(result);
});

请求管理层还需要实际接收 connection_done_rx,与原请求结果和业务截止时间协调。外层 oneshot 失败表示驱动任务没有正常报告结果;内层 Hyper Result 才是连接驱动结果。即使内层为 Ok,也只表示连接驱动正常结束,不证明某个请求获得了有效响应。

如果同一连接承载多个任务,应由连接管理者把终止状态通知到相关等待者;不能让多个请求争抢同一个 oneshot 接收端。已经完成的请求也不能因稍后正常关闭而被改判成失败。

确认连接已失效后,移出连接池并释放相应 sender 和所有副本,取消仍在等待的请求任务。先让借用 sender 的 future 退出,再释放 sender,保持 Rust 的借用与所有权关系。不要在健康响应 body 尚需 connection 驱动时,无条件 abort 驱动任务来“加快清理”。

不把重试与资源释放混为一谈

看到 body 终于被释放,并不表示请求从未被服务端处理。重试仍需考虑幂等性、body 是否可以重放,以及请求在失败前发送到了哪一步。直接重新使用已经消费的流式 body,可能制造另一个空请求问题。

截至 2026 年 10 月 23 日,已核对的 Hyper 1.12.0 正式源码还没有该关闭排空实现。更新依赖时应确认所用正式版本包含这项修正,并保留 Cargo.lock 与回退产物。

验证应包含无效 HTTP/1 响应、连接在请求发布附近关闭、正常完成以及业务取消,观察待处理请求是否有界结束、body 最终是否释放、健康连接是否仍被正确驱动。测试可以记录资源释放,但业务成败应由明确的请求与连接状态决定,不依赖未承诺的跨任务 Drop 顺序。