Rust in Everything
基于 hyper 1.12.0

hyper 解决什么问题

主线位置:两条主线的外面。本页回答 hyper 在一个 HTTP 程序里只管哪一段,两条主线的起点(接受连接、连接池)为什么在 1.0 被交给了 hyper-util 和使用者。
flowchart TB
  axum["axum:路由、提取器"] --> serve["axum::serve:accept 循环"]
  serve --> autob["hyper-util auto::Builder"]
  autob --> sconn["hyper server::conn"]
  reqwest["reqwest"] --> legacy["hyper-util legacy::Client"]
  legacy --> pool["连接池、HttpConnector"]
  pool --> cconn["hyper client::conn"]
  sconn --> io["字节流:rt::Read + Write"]
  cconn --> io
  io --> tio["TokioIo 包住 TcpStream"]

「低层」:管什么,不管什么

lib.rs 第 25 行的 crate 文档给自己的定位是:

hyper is a lower-level HTTP library, meant to be a building block
for libraries and applications.

If looking for just a convenient HTTP client, consider the
reqwest crate.

「building block」落到代码上,就是 server::conn 和 client::conn 两个模块:给它一条已经建立好的连接(任何实现了 rt::Read + rt::Write 的东西),它负责这条连接上的 HTTP。连接从哪里来、断了以后怎么办,它不管。两个模块的文档各有一句划界的话:

  • server::conn:「Accepting a connection and binding it with a service are not handled at this level.」
  • client::conn:「Connecting to a host, pooling connections, and the like are not handled at this level.」
事情谁来做
解析和编码 HTTP/1 报文,keep-alive,body 的长度和分块,协议升级hyper(proto/h1)
HTTP/2 的流和连接管理hyper 套在 h2 上(proto/h2)
accept 循环,每条连接 spawn 一个任务你自己,或者 axum 的 serve
一个端口同时接 HTTP/1 和 HTTP/2hyper-util 的 server::conn::auto
连接池、DNS、TCP 连接hyper-util 的 client::legacy
TLShyper 里没有任何 TLS 代码;用别的 crate 把 TLS 流包成 IO 交给 hyper
路由、提取器、中间件axum、tower
运行时:IO 就绪、spawn、计时通过 hyper::rt 的 trait 由使用者提供,tokio 的实现在 hyper-util

hyper 的 docs/VISION.md 写了为什么要这么低:它的主要用户本身就是库。「Low-Level Client Library (curl, reqwest, aws-sdk)」一节说这些客户端库「are expressing their own opinion on how a more-featured HTTP client should act. This includes opinions on connection establishment, management, pooling, HTTP version options, and even runtimes.」;「Web Server Frameworks (deno, axum)」一节说 hyper 要「flexible enough for them to build a server framework」。连接怎么建、池怎么管、用哪个运行时,这些都是上层库的「opinion」,hyper 不替它们决定。第 89 行把这一点概括成一句:「In all cases, a user brings their own runtime and IO to work with hyper.」

和 reqwest、axum 的关系

最常见的两个用法都不直接碰 hyper 的类型,中间隔着 hyper-util:

  • reqwest 0.13.5:内部的 HyperClient 就是 hyper_util::client::legacy::Client<Connector, Body>,用 Client::builder(hyper_util::rt::TokioExecutor::new()) 构造;连接器包着 hyper_util::client::legacy::connect::HttpConnector,再在外面加 TLS 和代理。也就是说,用 reqwest 发请求时走的就是本卷的客户端主线。
  • axum 0.8.9:axum::serve 自己写 accept 循环,每条连接用 hyper_util::server::conn::auto::Builder 的 serve_connection_with_upgrades,把 Router 用 TowerToHyperService 包一层交进去。这一段在 tower 卷的 Steer 与 MakeService 里已经拆开讲过。也就是说,axum 的每个请求走的是本卷的服务端主线。

所以读懂 hyper 的两条主线,reqwest 和 axum 底下发生的事也就清楚了。

1.0 为什么把 Server 和 Client 拿掉

hyper 0.14 有高层的 hyper::Server(自带 Accept trait 和 make_service_fn)和带连接池的 hyper::Client。1.0 把它们都删了。理由写在 1.0 的路线图 docs/ROADMAP-1.0.md 的「Higher-level Client and Server problems」一节,开头一句是「Both the higher-level Client and Server types have stability concerns.」,然后分别列问题。

hyper::Server 的问题:

  • 「The Accept trait is complex, and too easy to get wrong. If used with TLS, a slow TLS handshake can affect all other new connections waiting for it to finish.」
  • 「The MakeService<&IO> is confusing. The bounds are an assault on the eyes.」
  • 「The MakeService API doesn't allow to easily annotate the HTTP connection with tracing.」
  • 「Graceful shutdown doesn't give enough control.」

接着一句:「It's more common for people to simply use hyper::server::conn at this point, than to bother with the hyper::Server.」

hyper::Client 的问题:「The whole Connect design isn't stable.」(ALPN 和代理会带来额外的连接配置,有的连接器想看到路径而不只是 scheme、host、port),以及连接池「could be made more general or composable」,但更多定制该怎么暴露「it's not clear」。

结论在 「Focus on the Connection level」:

As mentioned in the *Known Issues*, the higher-level `Client` and `Server` have
stability and complexity problems. Therefore, for hyper 1.0, the main API will
focus on the "lower-level" connection types. The `Client` and `Server` helpers
will be moved to `hyper-util`.

照这个结论合并的是 #2932(删掉 hyper::Server、server::Builder、server::accept、make_service_fn,「New utilities for managing servers will exist in hyper-util.」)和 #2941(删掉 Client),都最早出现在 v1.0.0-rc.1。CHANGELOG 给 #2941 写的迁移说明只有一句:「A pooling client is in the hyper-util crate.」

同一份路线图的 「Split per HTTP version」还解释了为什么 1.x 的连接类型按版本拆成 http1 和 http2 两个模块:原来的 Connection 靠「generic AsyncRead + AsyncWrite transport」抽象掉版本,「But as we figure out HTTP/3, that needs to change.」HTTP/3 的流由 QUIC 创建,不能再假设底下是一条读写字节流。把两个版本合起来的「auto」类型,路线图说「It's likely that this "auto" type will start in hyper-util.」,也就是今天的 server::conn::auto。

0.14 的东西在 1.x 里去了哪:

hyper 0.14hyper 1.x
hyper::Server、Accept自己写 accept 循环 + server::conn::http1 / http2,或 hyper-util 的 auto、graceful
make_service_fn删除;每条连接给 serve_connection 传一个 service 即可
hyper::Client、client::connecthyper-util 的 client::legacy::Client 和 client::legacy::connect
hyper::Body收到的 body 是 hyper::body::Incoming;自己构造的用 http-body-util 的 Full、Empty 等
runtime、tcp featurehyper-util 的 TokioIo、TokioExecutor、TokioTimer
tower 的 Servicehyper 自己的 Service,见下一节
📝NOTE
没有蠢问题

问: hyper-util 还是 0.1,生产代码能依赖它吗?我写的库能在公开 API 里暴露它的类型吗?

答: 能依赖,但最好别公开暴露。路线图的 FAQ「Should I publicly depend on hyper-util?」说:「As with any pre-1.0 crate, you can publicly depend on it, but it is explicitly less stable.」以及「In most cases, it's recommended to not publicly expose your dependency on hyper-util.」如果你的库用到了 hyper-util 的 trait,建议定义自己的抽象,内部再做一层适配。

Service 为什么没有 poll_ready

hyper 0.14 直接用 tower 的 Service,有 poll_ready(tower 为什么需要它,见 tower 卷的 Service trait)。1.0 的 hyper::service::Service 只剩一个方法:

pub trait Service<Request> {
    /// Responses given by the service.
    type Response;

    /// Errors produced by the service.
    // ...
    type Error;

    /// The future response value.
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    /// Process the request and return the response asynchronously.
    /// `call` takes `&self` instead of `mut &self` because:
    /// - It prepares the way for async fn,
    ///   since then the future only borrows `&self`, and thus a Service can concurrently handle
    ///   multiple outstanding requests at once.
    /// - It's clearer that Services can likely be cloned.
    /// - To share state across clones, you generally need `Arc<Mutex<_>>`
    ///   That means you're not really using the `&mut self` and could do with a `&self`.
    ///   The discussion on this is here: <https://github.com/hyperium/hyper/issues/3040>
    fn call(&self, req: Request) -> Self::Future;
}

改动分两步,每一步 hyper 都留了原话:

  1. 去掉 poll_ready。路线图在「You call hyper, or hyper calls you?」里问:「Does poll_ready pull its complexity weight for servers?」;在 未决问题「Should there be a simplified hyper::Service trait, or should hyper depend on tower-service?」下面写:「It's not clear that the backpressure is something needed at the Server boundary, thus meaning we should remove poll_ready from hyper.」同一处还提到 tower 自己当时也没定下来的事:「if it should be changed to async fn call, and if poll_ready is the best way to handle backpressure.」issue #2853 定下做法:「a simplified Service trait that is only the call method」。#2920 实现,最早出现在 v1.0.0-rc.1,CHANGELOG 的迁移说明是「Change any manual impl tower::Service to implement hyper::service::Service instead. The poll_ready method has been removed.」
  2. call 改取 &self。issue #3040 在 1.0 正式版之前把 &self 和 &mut self 的利弊摆出来讨论,#3223 选了 &self,最早出现在 v1.0.0-rc.4。三条理由就是上面代码注释里的三条;反方的理由也写在 #3040 里:「If a Service has state (usually per-connection), it's cheaper to mutate it on each connection. Not being &mut would require internal synchronization.」

代价是 hyper 的 Service 和 tower 的 Service 不再是同一个 trait。tower 的服务要经过 hyper-util 的 TowerToHyperService 才能交给 hyper,它每个请求克隆一次服务、再走一遍 poll_ready + call。细节见 Service 与 Body。

📝NOTE
没有蠢问题

问: 没有了 poll_ready,服务处理得慢时,hyper 会不会一直从连接上读新请求、在内存里越堆越多?

答: 同一条 HTTP/1 连接上不会。Conn 读完一个请求头之后,读状态就离开了 Init,要等这个请求的响应连同 body 全部写完、刷出去,读写状态才一起回到 Init;在那之前 Dispatcher::poll_read_head 不会去解析下一个请求头。hyper 内部的 Dispatch trait 另有一个 poll_ready,服务端实现 dispatch::Server::poll_ready 在 in_flight(正在等的响应 future)还在时返回 Pending,是同一件事的另一道保险。所以一条连接同一时刻只有一个请求在处理。跨连接的总量则由你的 accept 循环决定:hyper 不 accept,自然也不替你限制连接数。细节见 Dispatcher。

drop 一个请求 future 会怎样

hyper 返回的 future 都可以随时 drop,这是官方支持的取消方式。但取消在线路上意味着什么,取决于协议。lib.rs 第 33 行的「Cancel safety」一节:

- **HTTP/1** has no in-protocol way to abort a single request without
  affecting the shared connection, so dropping an in-flight request future
  closes the underlying TCP connection. Any subsequent call on the same
  `SendRequest` returns a `canceled` error; the connection cannot be
  reused.
- **HTTP/2** resets the single stream with `RST_STREAM` (`CANCEL` error
  code) and notifies the peer immediately rather than continuing to
  deliver a response body that would be discarded. The shared connection
  stays usable for other in-flight and future requests.

HTTP/1 的报文没有「请求 ID」,响应只能按顺序和请求对上。请求已经写出去了,响应还没读完,这时调用方不要了:连接上剩下的字节属于一个没人要的响应,要么把它读完扔掉,要么关连接。hyper 选择关连接。下面的程序验证这一点:服务端每个请求睡 200ms,客户端只等 50ms 就放弃,然后在同一个 SendRequest 上再试一次。

hyper = { version = "=1.12.0", features = ["client", "server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt", "net", "time"] }
use std::convert::Infallible;
use std::time::Duration;

use bytes::Bytes;
use http_body_util::Empty;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
  let listener = TcpListener::bind("127.0.0.1:0").await?;
  let addr = listener.local_addr()?;

  // 服务端:每个请求睡 200ms 再回
  let server = tokio::spawn(async move {
    let (stream, _) = listener.accept().await.unwrap();
    let svc = hyper::service::service_fn(|_req| async {
      tokio::time::sleep(Duration::from_millis(200)).await;
      Ok::<_, Infallible>(Response::new(Empty::<Bytes>::new()))
    });
    hyper::server::conn::http1::Builder::new()
      .serve_connection(TokioIo::new(stream), svc)
      .await
  });

  let stream = TcpStream::connect(addr).await?;
  let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
  let client_conn = tokio::spawn(conn);

  let req = || Request::get("/").header("host", "x").body(Empty::<Bytes>::new()).unwrap();

  // 第一个请求只等 50ms,超时后请求 future 被 drop
  let first = tokio::time::timeout(Duration::from_millis(50), sender.send_request(req())).await;
  println!("first:  {}", if first.is_err() { "timed out, future dropped" } else { "finished" });

  // 同一个 SendRequest:先等它就绪,再发一个
  match sender.ready().await {
    Ok(()) => println!("ready:  Ok"),
    Err(e) => println!("ready:  Err({e:?})"),
  }
  match sender.send_request(req()).await {
    Ok(resp) => println!("second: {}", resp.status()),
    Err(e) => println!("second: {e:?}"),
  }
  println!("client conn task: {:?}", client_conn.await?);
  println!("server conn task: {:?}", server.await?);
  Ok(())
}
❗IMPORTANT
动脑筋:第二次 ready() 返回什么?服务端的连接任务以什么结果结束?

平台 M2 Max、rustc 1.99.0。第一个请求已经写到了服务端,服务端正在 sleep 里;客户端放弃后,sender.ready() 是 Ok 还是错误?服务端那边的 serve_connection 是 Ok(()) 还是错误,如果是错误,是哪一种?

HTTP/2 的情况不同:每个请求是一个独立的流,单独发 RST_STREAM 就能取消,连接继续用。这一点放在 05 HTTP/2 讲。

📝NOTE
冷知识:「Cancel safety」这一节是 2026 年才写进文档的

取消行为在 v1.10.0 之前没有写进 hyper 的文档。issue #4054(「Document cancel safety」)问:「Would you accept a documentation patch describing Hyper's cancellation behavior?」,列出的要点就是「dropping an HTTP/1 request causes the connection to be closed」「dropping an HTTP/2 request leaves the shared connection in a good state and cancels the unwanted response (using RST_STREAM)」。

#4070 先在 HTTP/1、HTTP/2 两个 send_request 的文档里各加了一节「# Cancel safety」(HTTP/1 的在第 217 行),crate 文档只加了一句指路,随 v1.10.0 发布;PR 说明里写着服务端的行为留待以后,因为 issue 作者「hadn't verified it」。#4156 再把 crate 文档的那一句扩成上面引用的两段,理由是读者落到 crate 首页时真正想问的是「what happens on the wire when I drop this?」,随 v1.11.1 发布。本站锁的 1.10.1 只有前一半。

动脑筋答案

first:  timed out, future dropped
ready:  Err(hyper::Error(ChannelClosed))
second: hyper::Error(Canceled, "connection was not ready")
client conn task: Ok(())
server conn task: Err(hyper::Error(IncompleteMessage))

跑了 3 次,输出相同。逐行看:

  • ready: Err(ChannelClosed):请求 future 被 drop,它手里那个等响应的 oneshot 接收端也跟着 drop。连接任务里的 dispatch::Client::poll_ready 发现回调已被取消,返回 Err,Dispatcher 随即关闭连接,连接任务结束(第四行的 Ok(()))。SendRequest 再问「能发吗」,通道的另一头已经没了,得到 ChannelClosed。
  • second: Canceled, "connection was not ready":就是文档说的「returns a canceled error」。错误在 send_request 里当场产生:请求根本没放进通道。
  • server conn task: Err(IncompleteMessage):服务端读完了请求,正在等 service 的响应,这时读到 EOF。连接处在一个消息的中间,hyper 把它当作错误,Display 是「connection closed before message completed」。这条报文在常见坑里还会出现。

客户端这边一次 drop,换来的是整条连接作废:如果这是连接池里的连接,下一个请求要重新建连。

小结

  • hyper 是「building block」:只管已经建立好的一条连接上的 HTTP;accept、连接池、TLS、路由、运行时都在它外面。
  • reqwest 走 hyper-util 的 legacy::Client,axum 走 hyper-util 的 auto::Builder;两条主线就是它们底下的路径。
  • 1.0 删掉 Server 和 Client,路线图的原话是二者「have stability concerns」,主 API 改为聚焦连接级别,helpers 移到 hyper-util(#2932、#2941)。
  • Service 去掉 poll_ready(#2920,理由:服务端边界上是否需要背压「not clear」),call 改取 &self(#3223);代价是与 tower 的 Service 不兼容。
  • 同一条 HTTP/1 连接上,hyper 用内部的 Dispatch::poll_ready 保证只有一个请求在 service 里。
  • drop 一个 HTTP/1 请求 future 会关掉整条连接,之后的 send_request 返回 canceled;HTTP/2 只重置那一个流。

💬 关联讨论

发起讨论
加载中...