OnEarlyDrop:请求被放弃
主线位置:结局主线的第三问「什么算放弃」。作用点上横跨三个时刻:
call时造回调,响应 future 完成时把守卫交给 body 并造第二个回调,body 流动时判断有没有到结尾;真正的信号是Drop。
stateDiagram-v2 [*] --> FutureGuard: call 时造 future 回调 FutureGuard --> FutureFired: future 在出响应前被 drop FutureGuard --> BodyGuard: 内层返回 Ok 造 body 回调 FutureGuard --> Done: 内层返回 Err BodyGuard --> Done: 构造时 is_end_stream 为真 BodyGuard --> Done: poll_frame 返回 None 或 Err BodyGuard --> Done: 交出一帧后 is_end_stream 为真 BodyGuard --> BodyFired: body 在此之前被 drop FutureFired --> [*] BodyFired --> [*] Done --> [*]
要解决的问题
客户端在服务端还没回完时断开连接,HTTP 服务通常一无所知:hyper 发现连接没了,就把正在 poll 的响应 future 或 body 丢掉,handler 的 future 跟着被 drop,没有任何回调,也没有错误。on_early_drop/mod.rs 的模块文档第一段就是这个:「HTTP services typically learn nothing when a client disconnects mid-request. This middleware installs drop guards so that premature termination is observable: logs, metrics, cleanup.」
Trace 对此也无能为力:它的 ResponseFuture 被丢掉时什么都不调,ResponseBody 被丢掉时 on_eos 就是不来。InFlightRequests 能让计数归零,但分不清是正常结束还是被放弃。OnEarlyDrop 是 0.6.9 加的(#636,2026-05-05),把放弃分成两种事件:
- future drop:响应 future 在内层出结果之前被丢掉。客户端在 handler 还在算的时候走了。
- body drop:响应 body 在结尾之前被丢掉。响应头已经发出,body 发到一半连接断了。
守卫
两种事件用的是同一个守卫,OnEarlyDropGuard:
pub struct OnEarlyDropGuard<Callback: OnDropCallback> {
callback: Option<Callback>,
}
impl<Callback: OnDropCallback> OnEarlyDropGuard<Callback> {
pub fn completed(&mut self) {
self.callback = None;
}
}
impl<Callback: OnDropCallback> Drop for OnEarlyDropGuard<Callback> {
fn drop(&mut self) {
// Panicking in Drop aborts the process if we are already unwinding,
// so avoid `expect` here.
if let Some(callback) = self.callback.take() {
callback.on_drop();
}
}
}
默认是「会回调」,正常走完的路径要主动调 completed() 把回调扔掉。忘了标记的路径都会被当成放弃,这比「出事时记得调回调」安全:任何意料之外的退出路径,包括 panic 展开,都会触发它。注释提醒了一个约束:drop 可能发生在另一个 panic 展开的途中,这时再 panic 进程会直接 abort,所以 drop 里不用 expect,回调也不许 panic(OnDropCallback 的文档写着「Implementations must not panic」)。守卫是公开的,可以单独用来检测任何作用域的提前退出。
future 那一路:OnEarlyDropFuture::poll 只要内层 Ready,不管 Ok 还是 Err,都先标记完成:
// Inner resolved: suppress the future-drop guard for both Ok and Err.
if let Some(guard) = this.future_guard.as_mut() {
guard.completed();
}
*this.future_guard = None;
body 那一路:OnEarlyDropBody 有三处标记完成:
pub(crate) fn new(body: B, callback: Callback) -> Self
where
B: Body,
{
let mut guard = OnEarlyDropGuard::new(callback);
if body.is_end_stream() {
guard.completed();
}
Self { inner: body, guard }
}
// poll_frame 里:
if matches!(result, None | Some(Err(_))) {
this.guard.completed();
}
if matches!(result, Some(Ok(_))) && this.inner.is_end_stream() {
this.guard.completed();
}
构造时就是空的(HEAD、204),读到 None 或者 body 自己出错,交出一帧后内层报告 is_end_stream。body 出错不算放弃:注释说「service-level errors are out of scope for this middleware」,出错是 Trace 和分类器的事(见 Trace 的内部)。第三处是 0.7.0 的 #687 补的,和 Trace 的 on_eos 是同一个修复:长度精确的 body 交出最后一帧时 is_end_stream() 已经为真,使用方不会再 poll 一次去拿 None,之前这种正常读完的 body 被丢掉时会误报。
回调是工厂
回调不是在 drop 时才现造的,drop 发生时手里已经没有请求了。所以两个钩子 trait 都是「先造回调,存进守卫」:
OnFutureDrop<ReqB>:make(&mut self, &Request<ReqB>) -> Callback,call时调一次。闭包形式是|req| move || { ... },外层在请求时跑,内层在 drop 时跑。OnBodyDrop<ReqB>:两级,make_at_call在call时造一个中间值,make_at_response在响应头出来时拿中间值和response::Parts造最终回调。这样 body 的回调既能带上请求的 URI,也能带上响应的状态码。闭包要用OnBodyDropFn::new包一下,形式是|req| move |parts| move || { ... },三层。
两个槽都可以是 (),什么都不做。OnEarlyDropLayer::builder() 从两个 () 开始,.on_future_drop(..)、.on_body_drop(..) 各换一个;OnEarlyDropLayer::new(hook) 要求两个槽放同一个 hook,实际上只给 EarlyDropsAsFailures 用。
桥到 Trace:EarlyDropsAsFailures
开了 trace feature 才有的 EarlyDropsAsFailures 同时实现了两个钩子,把两种事件都变成一次 trace::OnFailure 调用,失败类型是 DroppedFailure:Future(FutureDropped) 或 Body(BodyDropped { status })。它在 call 时记下开始时间和 span:
fn make(&mut self, _request: &Request<ReqB>) -> Self::Callback {
FutureDropFailureCallback {
start: Instant::now(),
on_failure: self.on_failure.clone(),
span: Span::current(),
}
}
Span::current() 是「调用 make 的那一刻当前进入的 span」。Trace::call 在 span.enter() 之后才调内层的 call,所以 OnEarlyDrop 放在 TraceLayer 里面时,它抓到的是请求 span;放在外面,Trace 还没开始,抓到的是外层的 span(通常什么都没有)。模块文档写的是「Place this layer inside TraceLayer so that the emitted events inherit the request span」。
实测
第一部分用闭包把五种情况各走一遍;第二部分把 EarlyDropsAsFailures 分别放在 TraceLayer 里面和外面。时钟暂停,「客户端放弃」用 tokio::time::timeout 模拟:超时后 timeout 把它包着的 future 丢掉,和 hyper 在连接断开时丢掉 future 是一回事。
tower-http = { version = "=0.7.1", features = ["on-early-drop", "trace"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
futures = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body::Frame;
use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Empty, Full, StreamBody};
use std::{convert::Infallible, time::Duration};
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::{
on_early_drop::{EarlyDropsAsFailures, OnBodyDropFn, OnEarlyDropLayer},
trace::{DefaultMakeSpan, DefaultOnFailure, TraceLayer},
};
type Body = UnsyncBoxBody<Bytes, BoxError>;
async fn handler(req: Request<Full<Bytes>>) -> Result<Response<Body>, Infallible> {
let res = match req.uri().path() {
"/slow" => {
tokio::time::sleep(Duration::from_secs(5)).await;
Response::new(Empty::new().map_err(|e| match e {}).boxed_unsync())
}
"/stream" => {
let frames = [Ok::<_, BoxError>(Frame::data(Bytes::from("one"))), Ok(Frame::data(Bytes::from("two")))];
Response::new(StreamBody::new(futures::stream::iter(frames)).boxed_unsync())
}
"/no-content" => {
let mut r = Response::new(Empty::new().map_err(|e| match e {}).boxed_unsync());
*r.status_mut() = StatusCode::NO_CONTENT;
r
}
_ => Response::new(Full::from("hello").map_err(|e| match e {}).boxed_unsync()),
};
Ok(res)
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
tracing_subscriber::fmt().without_time().with_target(false).with_ansi(false).init();
// 1. 直接装闭包:future 一路是「请求时造回调」,body 一路多一层「响应头出来时」
let svc = ServiceBuilder::new()
.layer(
OnEarlyDropLayer::builder()
.on_future_drop(|req: &Request<Full<Bytes>>| {
let uri = req.uri().clone();
move || println!(" future dropped: {uri}")
})
.on_body_drop(OnBodyDropFn::new(|req: &Request<Full<Bytes>>| {
let uri = req.uri().clone();
move |parts: &http::response::Parts| {
let status = parts.status;
move || println!(" body dropped: {uri} ({status})")
}
})),
)
.service_fn(handler);
println!("/slow, caller gives up after 10ms");
let r = tokio::time::timeout(Duration::from_millis(10), svc.clone().oneshot(Request::get("/slow").body(Full::default()).unwrap())).await;
println!(" timed out: {}", r.is_err());
println!("/stream, read one frame then drop");
let mut body = svc.clone().oneshot(Request::get("/stream").body(Full::default()).unwrap()).await.unwrap().into_body();
let _ = body.frame().await;
drop(body);
println!("/stream, read to the end");
let body = svc.clone().oneshot(Request::get("/stream").body(Full::default()).unwrap()).await.unwrap().into_body();
let _ = body.collect().await;
println!("/ (Full), read the only frame, never poll again");
let mut body = svc.clone().oneshot(Request::get("/").body(Full::default()).unwrap()).await.unwrap().into_body();
let _ = body.frame().await;
drop(body);
println!("/no-content, dropped unread");
let body = svc.clone().oneshot(Request::get("/no-content").body(Full::default()).unwrap()).await.unwrap().into_body();
drop(body);
// 2. 桥到 trace::OnFailure:放在 TraceLayer 里面和外面各一次
let early = OnEarlyDropLayer::new(EarlyDropsAsFailures::new(DefaultOnFailure::default()));
// 默认 span 是 DEBUG 级,fmt 默认只开到 INFO,span 会被关掉;这里把 span 提到 INFO
let trace = TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().level(tracing::Level::INFO));
println!("EarlyDropsAsFailures inside TraceLayer");
let inside = ServiceBuilder::new().layer(trace.clone()).layer(early.clone()).service_fn(handler);
let _ = tokio::time::timeout(Duration::from_millis(10), inside.oneshot(Request::get("/slow").body(Full::default()).unwrap())).await;
println!("EarlyDropsAsFailures outside TraceLayer");
let outside = ServiceBuilder::new().layer(early).layer(trace).service_fn(handler);
let _ = tokio::time::timeout(Duration::from_millis(10), outside.oneshot(Request::get("/slow").body(Full::default()).unwrap())).await;
}
/slow 超时、/stream 读一帧就丢、/stream 读完、Full 只读唯一的一帧就丢(不再 poll 拿 None)、204 不读就丢。平台 M2 Max、rustc 1.99.0。
动脑筋答案
/slow, caller gives up after 10ms
future dropped: /slow
timed out: true
/stream, read one frame then drop
body dropped: /stream (200 OK)
/stream, read to the end
/ (Full), read the only frame, never poll again
/no-content, dropped unread
EarlyDropsAsFailures inside TraceLayer
ERROR request{method=GET uri=/slow version=HTTP/1.1}: response failed classification=response future dropped before completion latency=0 ms
EarlyDropsAsFailures outside TraceLayer
ERROR response failed classification=response future dropped before completion latency=0 ms
跑 3 次相同。只有前两种:
/slow:timeout到点丢掉 future,future 守卫没被标记,回调。timed out: true打印在回调之后:被.await消耗掉的Timeoutfuture 连同里面的服务 future,在let r = ...这条语句结束时就被 drop 了。/stream读一帧:流式 body 不知道自己还剩多少,交出one后is_end_stream()是假,丢掉时回调,带着响应状态码。/stream读完:collect一直读到None,标记完成。Full只读一帧:Full交出唯一的一帧后is_end_stream()为真,#687 那一处标记完成,没有误报。- 204:
Empty在构造时is_end_stream()就为真,守卫一开始就被标记。
第二部分两行的区别就在 span:放在 TraceLayer 里面,ERROR 事件挂在 request{method=GET uri=/slow ...} 下;放在外面,Span::current() 是空的,事件孤零零的,看不出是哪个请求。另外,两次 Trace 自己都没有打印任何东西:它的 future 被丢掉时不调回调,这正是需要 OnEarlyDrop 的原因。latency=0 ms 是因为 EarlyDropsAsFailures 也用标准库的 Instant,暂停的 tokio 时钟不影响它。
为了让请求 span 出现,例子把 DefaultMakeSpan 的级别从默认的 DEBUG 提到了 INFO,否则 fmt 的默认过滤会把 span 关掉,两行看起来就一样了。
问: 客户端断开以后,hyper 一定会马上丢掉 future 吗?
答: 不一定。OnEarlyDrop 只在 future 或 body 真正被 drop 的那一刻回调,而什么时候 drop 由驱动它的一方决定:连接层要先发现对端走了,才会丢掉手里正在 poll 的东西。这一点在 HTTP/1 和 HTTP/2 上、在不同的服务器实现里都不一样,本页没有测。OnEarlyDrop 报告的是「被丢了」,不知道为什么、也不保证多快。
小结
OnEarlyDrop(0.6.9)区分两种放弃:响应 future 在出结果前被丢、body 在结尾前被丢;内层出错和 body 出错都不算放弃。- 守卫默认会回调,正常路径主动
completed();drop里不expect,回调不许 panic。 - body 有三处算正常结束:构造时就空、读到
None或错误、交出一帧后is_end_stream()为真(0.7.0 #687 补上,避免长度精确的 body 误报)。 - 回调是工厂:future 一路在
call时造,body 一路在call和响应头时各造一级,所以能同时带上 URI 和状态码。 EarlyDropsAsFailures把事件交给trace::OnFailure,抓的是call那一刻的Span::current(),要放在TraceLayer里面。