hyper::Error
主线位置:两条主线的出口。服务端主线上,
serve_connection返回的Connectionfuture 以Err(hyper::Error)结束,服务手里的Incoming读 body 时也可能拿到它;客户端主线上,send_request的 future、Connectionfuture、响应 body 都以它报错。hyper-util 的 legacyClient再把它包一层。
flowchart TB err["Error:一个 Box 指针"] --> imp["ErrorImpl"] imp --> kind["kind: Kind(私有)"] imp --> cause["cause: Option Box dyn Error"] kind --> isx["is_* 方法"] kind --> disp["Display:一行固定文本"] cause --> src["source()"] src --> chain["底层错误的 source 链"] chain --> fs["find_source:is_timeout 用"]
结构:一个指针
error.rs 里,公开的 Error 只有一个字段,真正的内容在 ErrorImpl 里:
pub struct Error {
inner: Box<ErrorImpl>,
}
struct ErrorImpl {
kind: Kind,
cause: Option<Cause>,
}
Cause 是 Box<dyn StdError + Send + Sync>。外面套一层 Box,hyper::Error 就只有一个指针大,Result<T, hyper::Error> 不会因为错误变胖。源码里有一个测试 error_size_of 断言 size_of::<Error>() == size_of::<usize>();下面的小程序在 64 位上打印 8。代价是每构造一个错误要一次堆分配。
kind 是 Kind,pub(super),外面看不到:
#[derive(Debug)]
pub(super) enum Kind {
Parse(Parse),
User(User),
/// A message reached EOF, but is not complete.
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
IncompleteMessage,
/// A connection received a message (or bytes) when not waiting for one.
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
UnexpectedMessage,
/// A pending item was dropped before ever being processed.
Canceled,
/// Indicates a channel (client or body sender) is closed.
#[cfg(any(
all(feature = "http1", any(feature = "client", feature = "server")),
all(feature = "http2", feature = "client")
))]
ChannelClosed,
/// An `io::Error` that occurred while trying to read or write to a network stream.
#[cfg(all(
any(feature = "client", feature = "server"),
any(feature = "http1", feature = "http2")
))]
Io,
/// User took too long to send headers.
#[cfg(all(feature = "http1", feature = "server"))]
HeaderTimeout,
/// Error while reading a body from connection.
#[cfg(all(
any(feature = "client", feature = "server"),
any(feature = "http1", feature = "http2")
))]
Body,
/// Error while writing a body to connection.
#[cfg(all(
any(feature = "client", feature = "server"),
any(feature = "http1", feature = "http2")
))]
BodyWrite,
/// Error calling `AsyncWrite::shutdown()`.
#[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
Shutdown,
/// A general error from h2.
#[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
Http2,
}
一共 12 个变体,Parse 和 User 下面还有细分(Parse、Header、User)。几乎每个变体都挂着 cfg:只开 http1 的构建里没有 Http2,只开 server 的构建里没有 DispatchGone。Kind 不公开,这些随 feature 变化的变体就不会出现在公开 API 里,hyper 也可以自由地细分、合并错误种类,对外只承诺那几个 is_* 方法的含义。
只公开 is_*
外面能用的只有判断方法。hyper 1.12.0 共 11 个,每个都是对 kind 的一次 matches!,只有 is_timeout 例外:
| 方法 | 对应的 Kind |
|---|---|
is_parse | Parse(_),任何解析错误 |
is_parse_too_large | Parse(TooLarge) 或 Parse(UriTooLong) |
is_parse_status | Parse(Status) |
is_parse_version_h2 | Parse(VersionH2):HTTP/1 连接上收到 HTTP/2 连接前言 |
is_user | User(_) |
is_canceled | Canceled |
is_closed | ChannelClosed |
is_incomplete_message | IncompleteMessage |
is_body_write_aborted | User(BodyWriteAborted) |
is_shutdown | Shutdown |
is_timeout | HeaderTimeout,或者 source 链上有私有类型 TimedOut |
几个 Kind 没有对应的方法:Io、Body、BodyWrite、Http2、UnexpectedMessage。碰到它们,所有 is_* 都返回 false,只能看 Display 文本或者 source() 里的底层错误。
is_timeout 的后一半:
pub fn is_timeout(&self) -> bool {
#[cfg(all(feature = "http1", feature = "server"))]
if matches!(self.inner.kind, Kind::HeaderTimeout) {
return true;
}
self.find_source::<TimedOut>().is_some()
}
find_source 沿 source() 一层层往下找,能 downcast_ref 成 E 就返回。TimedOut 是一个零大小的私有哨兵类型,Display 是 operation timed out。HTTP/2 keep-alive 超时用的就是它:ping.rs 里的 KeepAliveTimedOut 包在 Kind::Http2 下面,它自己的 source 返回 &TimedOut。于是一个 Http2 错误也能 is_timeout(),而判断的依据(TimedOut 在链上的位置)对外不可见,hyper 以后改错误的层次也不影响这个方法。
is_shutdown 是 1.7.0 才加的(#3863),提交说明给的理由是「Benchmarking tools often cause connections to be ungracefully shutdown. This makes it possible to filter these errors out.」压测工具不等响应就断开,服务端关写方向时出错,日志里这类错误可以靠它过滤掉。
内部要按 Kind 分支的地方用 kind(),它是 pub(super),只在 http1 + server(或 ffi)下编译。HTTP/1 服务端决定回 400、414 还是 431 的 role::Server::on_error 就是用它 match *err.kind(),那张表见 解析与编码。
Display 只打一层,原因在 source() 里
Display 只写一段由 kind 决定的固定文本,不管 cause:
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.description())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.inner
.cause
.as_ref()
.map(|cause| &**cause as &(dyn StdError + 'static))
}
}
description 是一个大 match,每个变体一句话。所以 println!("{e}") 对一个连接被重置的错误只打印 connection error,「Connection reset by peer」要到 source() 里找。要看全,就自己沿 source() 走一遍(下面实验里的 chain 函数),或者用 anyhow 一类库的 {:#}、标准库还不稳定的 std::error::Report。Error 的文档也这样说:「To print all the relevant information, including the source chain, using something like std::error::Report, or equivalent 3rd party types.」同一段还声明 Display 的措辞「is unspecified. You must not depend on it.」
Display 会打印整条链hyper 0.14 的 Error 打印时会把 cause 接在后面。issue #2844 记下了问题:这和生态里「Display 只打印自己、原因交给 source()」的约定冲突,要么改 hyper,要么说服错误处理工作组允许 Display 带上链。最后改的是 hyper:PR #3312「hyper's Error used to print the error source automatically, preferring to provide a better default for users who do not know about Report. But, to fit better with the wider ecosystem, this changes the format to only print the hyper Error itself, and not its source.」第一个包含它的发布是 v1.0.0。这是一个破坏性变更,提交说明里专门提醒「Be sure to check if you are logging errors directly.」,同时删掉了 Error::message() 和 Error::into_cause()。
后果是:直接 {e} 打日志的代码,升级到 1.0 后日志里少了最有用的那半句。上面那个 connection error 就是例子。
source() 返回的底层错误能不能 downcast 成具体类型来判断?lib.rs 列出了不属于稳定 API 的部分,第一条就是「Downcasting error types from Error::source() is not considered stable.」这同样是 1.0 定下的,PR #3318:「Do not build any logic depending on the exact types of an Error::source(). They are only for debugging.」Error 的文档解释了原因:这些错误「may come from private internal dependencies」,比如 HTTP/2 下面的 h2,hyper 换掉或升级依赖时类型就会变。实践中,用 is_* 能判断的就用 is_*;判断不了的(比如 Io 下面是哪种 io::ErrorKind),自己承担升级时要重新核对的代价。
Debug 和 Display 不同,会把私有的 Kind 打出来(依赖和下一节的实验相同):
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::Empty;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
async fn handle(_: Request<Incoming>) -> Result<Response<Empty<Bytes>>, Infallible> {
Ok(Response::new(Empty::new()))
}
#[tokio::main]
async fn main() {
for bytes in [&b"G@T / HTTP/1.1\r\n\r\n"[..], &b"GET / HTTP/1.1\r\n"[..]] {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let mut tcp = TcpStream::connect(addr).await.unwrap();
tcp.write_all(bytes).await.unwrap();
tcp.shutdown().await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
});
let (stream, _) = listener.accept().await.unwrap();
let e = http1::Builder::new().serve_connection(TokioIo::new(stream), service_fn(handle)).await.unwrap_err();
println!("Display: {e}");
println!("Debug: {e:?}");
println!("size_of::<hyper::Error>() = {}", std::mem::size_of::<hyper::Error>());
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
Display: invalid HTTP method parsed
Debug: hyper::Error(Parse(Method))
size_of::<hyper::Error>() = 8
Display: connection closed before message completed
Debug: hyper::Error(IncompleteMessage)
size_of::<hyper::Error>() = 8
Debug 是手写的 debug_tuple("hyper::Error"),先放 kind(派生的 Debug),有 cause 再放 cause。这对排查有用,但和 Display 一样不该拿来做判断。
实验:逐个触发每种 Kind
下面的程序在一个进程里制造尽可能多的错误:服务端一侧,用裸 TcpStream 发手写字节给 hyper 的 http1 / http2 服务端;客户端一侧,用一个手写的 TCP 服务端回奇怪的东西给 hyper 的 http1 客户端;最后是 hyper-util 的 legacy Client。每个错误打印 Display、source() 链(用 -> 连接)、所有为 true 的 is_*。服务端的用例还打印客户端在线上收到的第一行(wire:)。
写 socket 刷新失败、关闭写方向失败这两种情况在本机 TCP 上很难稳定触发,程序用一个 Faulty IO 包住 TokioIo<TcpStream>,在 poll_flush / poll_shutdown 里返回人为的错误。
hyper = { version = "=1.12.0", features = ["client", "server", "http1", "http2"] }
hyper-util = { version = "=0.1.21", features = ["client-legacy", "http1", "tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync"] }
use std::error::Error as StdError;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full, StreamBody};
use hyper::body::{Body, Frame, Incoming, SizeHint};
use hyper::client::conn::http1 as client_h1;
use hyper::rt::{Read, ReadBufCursor, Write};
use hyper::server::conn::{http1, http2};
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
type BoxError = Box<dyn StdError + Send + Sync>;
type B = BoxBody<Bytes, BoxError>;
// ---------- printing ----------
fn chain(e: &(dyn StdError + 'static)) -> String {
let mut s = String::new();
let mut src = e.source();
while let Some(inner) = src {
s.push_str(&format!(" -> {inner}"));
src = inner.source();
}
s
}
fn report(label: &str, e: &hyper::Error) {
let checks: [(&str, bool); 11] = [
("parse", e.is_parse()),
("parse_too_large", e.is_parse_too_large()),
("parse_status", e.is_parse_status()),
("parse_version_h2", e.is_parse_version_h2()),
("user", e.is_user()),
("canceled", e.is_canceled()),
("closed", e.is_closed()),
("incomplete_message", e.is_incomplete_message()),
("body_write_aborted", e.is_body_write_aborted()),
("shutdown", e.is_shutdown()),
("timeout", e.is_timeout()),
];
let yes: Vec<&str> = checks.iter().filter(|(_, b)| *b).map(|(n, _)| *n).collect();
println!("{label:<24} {e}{}", chain(e));
println!("{:<24} is_*: {}", "", if yes.is_empty() { "(none)".to_string() } else { yes.join(", ") });
}
// ---------- an IO wrapper that fails flush or shutdown on demand ----------
struct Faulty {
inner: TokioIo<TcpStream>,
fail_flush: bool,
fail_shutdown: bool,
}
impl Read for Faulty {
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: ReadBufCursor<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
impl Write for Faulty {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
if self.fail_flush {
return Poll::Ready(Err(io::Error::other("injected flush error")));
}
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
if self.fail_shutdown {
return Poll::Ready(Err(io::Error::other("injected shutdown error")));
}
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
// ---------- the server's service ----------
// Claims 5 bytes, delivers 3.
struct Short(bool);
impl Body for Short {
type Data = Bytes;
type Error = BoxError;
fn poll_frame(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Result<Frame<Bytes>, BoxError>>> {
if self.0 {
return Poll::Ready(None);
}
self.0 = true;
Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"abc")))))
}
fn size_hint(&self) -> SizeHint {
SizeHint::with_exact(5)
}
}
fn full(s: &'static str) -> B {
Full::new(Bytes::from(s)).map_err(|e| match e {}).boxed()
}
async fn handle(mut req: Request<Incoming>) -> Result<Response<B>, BoxError> {
let res = match req.uri().path() {
"/svc-err" => return Err("boom".into()),
"/body-err" => {
let frames: Vec<Result<Frame<Bytes>, BoxError>> = vec![Ok(Frame::data(Bytes::from("a"))), Err("bad chunk".into())];
Response::new(StreamBody::new(futures::stream::iter(frames)).boxed())
}
"/short" => Response::new(Short(false).boxed()),
"/103" => Response::builder().status(StatusCode::EARLY_HINTS).body(full("")).unwrap(),
"/two-cl" => {
// a body of unknown length, so hyper has to trust the headers
let frames: Vec<Result<Frame<Bytes>, BoxError>> = vec![Ok(Frame::data(Bytes::from("abc")))];
let mut res = Response::new(StreamBody::new(futures::stream::iter(frames)).boxed());
res.headers_mut().append("content-length", "3".parse().unwrap());
res.headers_mut().append("content-length", "4".parse().unwrap());
res
}
"/no-upgrade" => {
if let Err(e) = hyper::upgrade::on(&mut req).await {
report(" upgrade::on", &e);
}
Response::new(full("ok"))
}
"/upgrade" => {
let on = hyper::upgrade::on(&mut req);
tokio::spawn(async move {
if let Err(e) = on.await {
report(" upgrade::on", &e);
}
});
Response::builder()
.status(StatusCode::SWITCHING_PROTOCOLS)
.header("connection", "upgrade")
.header("upgrade", "foo")
.body(full(""))
.unwrap()
}
"/read-body" => {
if let Err(e) = req.into_body().collect().await {
report(" request body", &e);
}
Response::new(full("ok"))
}
_ => Response::new(full("ok")),
};
Ok(res)
}
// ---------- harnesses ----------
#[derive(Clone, Copy)]
enum Then {
ReadToEnd, // write the bytes, read until the server closes
ShutdownWrite, // write the bytes, close our write half, read until close
Reset, // write the bytes, then RST the connection
}
#[derive(Clone, Copy, Default)]
struct Opts {
header_timeout: bool,
fail_flush: bool,
fail_shutdown: bool,
}
async fn server_case(label: &str, bytes: &'static [u8], then: Then, opts: Opts) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let client = tokio::spawn(async move {
let mut tcp = TcpStream::connect(addr).await.unwrap();
let _ = tcp.write_all(bytes).await;
match then {
Then::ReadToEnd => {}
Then::ShutdownWrite => {
let _ = tcp.shutdown().await;
}
Then::Reset => {
tokio::time::sleep(Duration::from_millis(50)).await;
#[allow(deprecated)]
tcp.set_linger(Some(Duration::ZERO)).unwrap();
drop(tcp);
return String::from("(reset by client)");
}
}
let mut got = Vec::new();
let _ = tcp.read_to_end(&mut got).await;
match String::from_utf8_lossy(&got).lines().next() {
Some(line) => line.to_string(),
None => String::from("(nothing)"),
}
});
let (stream, _) = listener.accept().await.unwrap();
let io = Faulty { inner: TokioIo::new(stream), fail_flush: opts.fail_flush, fail_shutdown: opts.fail_shutdown };
let mut b = http1::Builder::new();
if opts.header_timeout {
b.timer(TokioTimer::new()).header_read_timeout(Duration::from_millis(100));
}
match tokio::time::timeout(Duration::from_secs(2), b.serve_connection(io, service_fn(handle))).await {
Ok(Ok(())) => println!("{label:<24} Ok(())"),
Ok(Err(e)) => report(label, &e),
Err(_) => println!("{label:<24} still running after 2 s"),
}
if let Ok(Ok(line)) = tokio::time::timeout(Duration::from_millis(500), client).await {
println!("{:<24} wire: {line}", "");
}
}
// A raw server that runs `script` on the accepted socket; returns its address.
async fn raw_server<F, Fut>(script: F) -> std::net::SocketAddr
where
F: FnOnce(TcpStream) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send,
{
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
script(stream).await;
});
addr
}
async fn read_head(tcp: &mut TcpStream) {
let mut buf = vec![0u8; 4096];
let mut got = Vec::new();
while !got.windows(4).any(|w| w == b"\r\n\r\n") {
let n = tcp.read(&mut buf).await.unwrap();
if n == 0 {
return;
}
got.extend_from_slice(&buf[..n]);
}
}
async fn h1_client(addr: std::net::SocketAddr) -> (client_h1::SendRequest<Empty<Bytes>>, client_h1::Connection<TokioIo<TcpStream>, Empty<Bytes>>) {
let io = TokioIo::new(TcpStream::connect(addr).await.unwrap());
client_h1::handshake(io).await.unwrap()
}
fn get(path: &str) -> Request<Empty<Bytes>> {
Request::get(path).body(Empty::new()).unwrap()
}
#[tokio::main]
async fn main() {
let d = Opts::default();
println!("--- server side: hyper::server::conn::http1, raw TCP client");
server_case("bad method", b"G@T / HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("bad uri", b"GET /a\x7fb HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("bad version", b"GET / HTTP/1.2\r\n\r\n", Then::ReadToEnd, d).await;
server_case("h2 preface", b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", Then::ReadToEnd, d).await;
server_case("bad header", b"GET / HTTP/1.1\r\nbad header: x\r\n\r\n", Then::ReadToEnd, d).await;
server_case("content-length abc", b"POST / HTTP/1.1\r\ncontent-length: abc\r\n\r\n", Then::ReadToEnd, d).await;
server_case("te: gzip", b"POST / HTTP/1.1\r\ntransfer-encoding: gzip\r\n\r\n", Then::ReadToEnd, d).await;
server_case("te on HTTP/1.0", b"POST / HTTP/1.0\r\ntransfer-encoding: chunked\r\n\r\n", Then::ReadToEnd, d).await;
let many: &'static str = Box::leak(
format!("GET / HTTP/1.1\r\n{}\r\n", (0..101).map(|i| format!("x-h{i}: v\r\n")).collect::<String>()).into_boxed_str(),
);
server_case("101 headers", many.as_bytes(), Then::ReadToEnd, d).await;
let long: &'static str = Box::leak(format!("GET /{} HTTP/1.1\r\n\r\n", "a".repeat(65534)).into_boxed_str());
server_case("uri 65535 bytes", long.as_bytes(), Then::ReadToEnd, d).await;
let to = Opts { header_timeout: true, ..d };
server_case("slow head", b"GET / HTTP/1.1\r\n", Then::ReadToEnd, to).await;
server_case("EOF mid head", b"GET / HTTP/1.1\r\n", Then::ShutdownWrite, d).await;
server_case("RST mid head", b"GET / HTTP/1.1\r\n", Then::Reset, d).await;
server_case("EOF mid body", b"POST /read-body HTTP/1.1\r\ncontent-length: 10\r\n\r\nabc", Then::ShutdownWrite, d).await;
server_case("RST mid body", b"POST /read-body HTTP/1.1\r\ncontent-length: 10\r\n\r\nabc", Then::Reset, d).await;
server_case("service Err", b"GET /svc-err HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("body Err", b"GET /body-err HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("body too short", b"GET /short HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("status 103", b"GET /103 HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("two content-lengths", b"GET /two-cl HTTP/1.1\r\n\r\n", Then::ReadToEnd, d).await;
server_case("upgrade::on, no upgrade", b"GET /no-upgrade HTTP/1.1\r\nconnection: close\r\n\r\n", Then::ReadToEnd, d).await;
server_case(
"101 w/o with_upgrades",
b"GET /upgrade HTTP/1.1\r\nconnection: upgrade\r\nupgrade: foo\r\n\r\n",
Then::ReadToEnd,
d,
)
.await;
tokio::time::sleep(Duration::from_millis(50)).await;
let ff = Opts { fail_flush: true, ..d };
server_case("flush fails", b"GET / HTTP/1.1\r\n\r\n", Then::ReadToEnd, ff).await;
let fs = Opts { fail_shutdown: true, ..d };
server_case("shutdown fails", b"GET / HTTP/1.1\r\nconnection: close\r\n\r\n", Then::ReadToEnd, fs).await;
println!("--- server side: hyper::server::conn::http2");
{
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let client = tokio::spawn(async move {
let mut tcp = TcpStream::connect(addr).await.unwrap();
tcp.write_all(b"GET / HTTP/1.1\r\n\r\n").await.unwrap();
let mut sink = Vec::new();
let _ = tcp.read_to_end(&mut sink).await;
});
let (stream, _) = listener.accept().await.unwrap();
let r = http2::Builder::new(TokioExecutor::new()).serve_connection(TokioIo::new(stream), service_fn(handle)).await;
if let Err(e) = r {
report("h1 bytes to h2 server", &e);
}
let _ = client.await;
}
{
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let client = tokio::spawn(async move {
let mut tcp = TcpStream::connect(addr).await.unwrap();
// preface + empty SETTINGS, then never answer anything (no PING ack)
tcp.write_all(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00\x00\x04\x00\x00\x00\x00\x00").await.unwrap();
let mut sink = Vec::new();
let _ = tcp.read_to_end(&mut sink).await;
});
let (stream, _) = listener.accept().await.unwrap();
let r = http2::Builder::new(TokioExecutor::new())
.timer(TokioTimer::new())
.keep_alive_interval(Duration::from_millis(100))
.keep_alive_timeout(Duration::from_millis(100))
.serve_connection(TokioIo::new(stream), service_fn(handle))
.await;
if let Err(e) = r {
report("h2 ping unanswered", &e);
}
let _ = client.await;
}
println!("--- client side: hyper::client::conn::http1, raw TCP server");
{
let addr = raw_server(|mut tcp| async move {
read_head(&mut tcp).await;
drop(tcp);
})
.await;
let (mut tx, conn) = h1_client(addr).await;
tokio::spawn(conn);
if let Err(e) = tx.send_request(get("/")).await {
report("closed before response", &e);
}
}
{
let addr = raw_server(|mut tcp| async move {
read_head(&mut tcp).await;
tcp.write_all(b"HTTP/1.1 abc OK\r\n\r\n").await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
})
.await;
let (mut tx, conn) = h1_client(addr).await;
tokio::spawn(conn);
if let Err(e) = tx.send_request(get("/")).await {
report("bad status line", &e);
}
}
{
let addr = raw_server(|mut tcp| async move {
tcp.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n").await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
})
.await;
let (_tx, conn) = h1_client(addr).await;
if let Err(e) = conn.await {
report("response before request", &e);
}
}
{
let addr = raw_server(|mut tcp| async move {
read_head(&mut tcp).await;
tokio::time::sleep(Duration::from_millis(100)).await;
})
.await;
let (mut tx, conn) = h1_client(addr).await;
tokio::spawn(conn);
let _first = tx.send_request(get("/1"));
if let Err(e) = tx.send_request(get("/2")).await {
report("2nd send, not ready", &e);
}
}
{
let addr = raw_server(|tcp| async move {
drop(tcp);
})
.await;
let (mut tx, conn) = h1_client(addr).await;
let _ = conn.await;
if let Err(e) = tx.ready().await {
report("ready() after conn ended", &e);
}
}
{
let (got_tx, got_rx) = tokio::sync::oneshot::channel();
let addr = raw_server(|mut tcp| async move {
read_head(&mut tcp).await;
let _ = got_tx.send(());
tokio::time::sleep(Duration::from_millis(200)).await;
})
.await;
let (mut tx, conn) = h1_client(addr).await;
let task = tokio::spawn(conn);
let fut = tx.send_request(get("/"));
let waiter = tokio::spawn(fut);
got_rx.await.unwrap();
task.abort();
if let Err(e) = waiter.await.unwrap() {
report("conn task aborted", &e);
}
}
println!("--- hyper_util::client::legacy::Client");
let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build_http();
{
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let uri = format!("http://{addr}/").parse().unwrap();
if let Err(e) = client.get(uri).await {
println!("{:<24} {e}{}", "connection refused", chain(&e));
println!("{:<24} is_connect: {}", "", e.is_connect());
}
}
{
let addr = raw_server(|mut tcp| async move {
read_head(&mut tcp).await;
drop(tcp);
})
.await;
let uri = format!("http://{addr}/").parse().unwrap();
if let Err(e) = client.get(uri).await {
println!("{:<24} {e}{}", "closed before response", chain(&e));
let inner = e.source().and_then(|s| s.downcast_ref::<hyper::Error>());
println!("{:<24} is_connect: {}, source is hyper::Error: {}", "", e.is_connect(), inner.is_some());
}
}
}
M2 Max(macOS,os error 的编号是 macOS 的)、rustc 1.99.0、--release,连跑 8 次。下面分段引用其中一次的输出,各段按原顺序首尾相接,只把中间两个用例留到「动脑筋答案」。8 次之间的差别有两处,在对应的段落后面说明。
解析错误
--- server side: hyper::server::conn::http1, raw TCP client
bad method invalid HTTP method parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
bad uri invalid URI
is_*: parse
wire: HTTP/1.1 400 Bad Request
bad version invalid HTTP version parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
h2 preface invalid HTTP version parsed (found HTTP2 preface)
is_*: parse, parse_version_h2
wire: (nothing)
bad header invalid HTTP header parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
content-length abc invalid content-length parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
te: gzip invalid transfer-encoding parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
te on HTTP/1.0 unexpected transfer-encoding parsed
is_*: parse
wire: HTTP/1.1 400 Bad Request
101 headers message head is too large
is_*: parse, parse_too_large
wire: HTTP/1.1 431 Request Header Fields Too Large
uri 65535 bytes URI too long
is_*: parse, parse_too_large
wire: HTTP/1.1 414 URI Too Long
Parse 连同 Header 下的 4 种一共 12 种细分,这一段触发了 10 种,Status 在后面的客户端一段。唯一没触发的是 Parse::Internal(「internal error inside Hyper and/or its dependencies, please report」),它只在 role.rs 的 maybe_panic! 里产生:debug 构建直接 panic,release 构建打一条日志、返回这个错误,是给「不该发生」的情况留的。
bad uri:路径里有一个0x7f。httparse 报的是通用的Token错误,hyper 在Server::parse里看方法是否已经解析出来:没有就是Parse::Method,有就是Parse::Uri。uri 65535 bytes是Parse::UriTooLong,is_parse_too_large也为true,方法的实现把它和TooLarge归在一起。101 headers是超过max_headers的TooLarge,同样为true,虽然这个方法的文档只提到max_buf_size和 URI 长度。wire一列就是 解析与编码 里那张表:解析错误只回 400、414、431,HTTP/2 连接前言不回任何东西。
连接层面的错误
slow head read header from client timeout
is_*: timeout
wire: (nothing)
EOF mid head connection closed before message completed
is_*: incomplete_message
wire: (nothing)
RST mid head connection error -> Connection reset by peer (os error 54)
is_*: (none)
wire: (reset by client)
request body error reading a body from connection -> end of file before message length reached
is_*: (none)
EOF mid body Ok(())
wire: HTTP/1.1 200 OK
request body error reading a body from connection -> Connection reset by peer (os error 54)
is_*: (none)
RST mid body error writing a body to connection -> Broken pipe (os error 32)
is_*: (none)
wire: (reset by client)
HeaderTimeout(slow head):读头超时,is_timeout。hyper 不回 408,直接关连接。IncompleteMessage(EOF mid head):请求头读到一半遇到 EOF。客户端一侧最常见的connection closed before message completed也是它,见下文。Io(RST mid head):读 socket 出错,Display只有connection error,原因在 source 里。没有is_*能认出它。Body:两个request body行是服务里collect()请求 body 时拿到的错误。缩进的行先打印,因为服务先看到 body 出错,Connection后结束。EOF mid body的 body 只来了 10 个字节里的 3 个就 EOF,底层是Decoder产生的io::ErrorKind::UnexpectedEof;注意它不是IncompleteMessage,is_incomplete_message为false,语义相近的两种情况落在两个 Kind 上。这条连接本身以Ok(())结束,服务的ok响应照常写了出去。BodyWrite(RST mid body):客户端发 RST 后,服务端写响应时 flush 失败。Dispatcher::poll_flush把所有 flush 错误都包成new_body_write,所以连写响应头时的失败也叫「error writing a body」。
用户代码的错误
body too short user body write aborted -> early end, expected 2 more bytes
is_*: user, body_write_aborted
wire: (nothing)
status 103 response has 1xx status code, not supported by server
is_*: user
wire: HTTP/1.1 500 Internal Server Error
two content-lengths user sent unexpected header
is_*: user
wire: (nothing)
upgrade::on no upgrade available
is_*: user
upgrade::on, no upgrade Ok(())
wire: HTTP/1.1 200 OK
101 w/o with_upgrades Ok(())
upgrade::on upgrade expected but low level API in use
is_*: user
wire: HTTP/1.1 101 Switching Protocols
flush fails error writing a body to connection -> injected flush error
is_*: (none)
wire: HTTP/1.1 200 OK
shutdown fails error shutting down connection -> injected shutdown error
is_*: shutdown
wire: HTTP/1.1 200 OK
User(BodyWriteAborted)(body too short):body 的size_hint说 5 个字节,只给了 3 个就结束。Encoder收尾时发现长度不够,source 是early end, expected 2 more bytes。User(UnsupportedStatusCode)(status 103):服务返回 1xx(101 除外)。Server::encode把响应头换成500,写出去,再报这个错。这是服务端唯一一种「服务的问题导致 hyper 自己回 500」的情况。User(UnexpectedHeader)(two content-lengths):body 长度未知(这里用StreamBody),响应头里两个content-length值不同。编码时回滚已写的头,什么都不发。要是 body 长度已知(比如Full),hyper 直接相信 body 的长度,不会走到这里。User(NoUpgrade)和User(ManualUpgrade):前者是对一个不带升级头的请求调hyper::upgrade::on;后者是服务回了101,但连接没有用with_upgrades,Connection结束时只能通知OnUpgrade「升级不了」,见 协议升级。这两行和101 w/o with_upgrades Ok(())的先后在 8 次里变了 3 次:通知在另一个任务里打印,和连接结束谁先打印取决于调度。BodyWrite(flush fails):人为让poll_flush失败。poll_write已经把响应字节交给了 socket,客户端多数时候照样收到200 OK;8 次里有 1 次wire是(nothing)。Shutdown(shutdown fails):connection: close的请求处理完,Dispatcher调poll_shutdown关写方向时出错,is_shutdown为true。响应早已写完。
HTTP/2
--- server side: hyper::server::conn::http2
h1 bytes to h2 server http2 error -> connection error detected: unspecific protocol error detected
is_*: (none)
h2 ping unanswered http2 error -> keep-alive timed out -> operation timed out
is_*: timeout
Http2 是 h2 crate 报上来的错误(Error::new_h2:h2 的 IO 错误归 Io,其余归 Http2),source 是 h2 的 Error,这里是 PROTOCOL_ERROR。第二行是前面讲的 keep-alive 超时:Kind 是 Http2,链上有 TimedOut,is_timeout 为 true。
客户端
--- client side: hyper::client::conn::http1, raw TCP server
closed before response connection closed before message completed
is_*: incomplete_message
bad status line invalid HTTP status-code parsed
is_*: parse, parse_status
response before request received unexpected message from connection
is_*: (none)
2nd send, not ready operation was canceled -> connection was not ready
is_*: canceled
ready() after conn ended channel closed
is_*: closed
conn task aborted dispatch task is gone -> runtime dropped the dispatch task
is_*: user
IncompleteMessage:请求写出去了,服务端读完请求头就关连接。这是客户端最常见的报错,典型来源是连接池复用了一条服务端刚关掉的空闲连接。Parse(Status):状态行HTTP/1.1 abc OK。UnexpectedMessage:客户端还没发请求,服务端先发来一个响应。Conn::require_empty_read在空闲时检查读缓冲,有字节就报这个错,Connectionfuture 以它结束。Canceled:不等ready()就在 HTTP/1 的SendRequest上连发两个请求,第二个在send_request里就被拒,source 是connection was not ready。SendRequest什么时候算 ready 见 conn 客户端。ChannelClosed:连接已经结束,再ready(),is_closed为true。User(DispatchGone):请求已经被连接任务取走,连接任务却被 abort 了。回调在Callback的Drop里发出dispatch_gone(),source 是runtime dropped the dispatch task(如果是 panic 引起的则是user code panicked)。它算User错误,is_user为true。
12 个 Kind 全部出现过;User 下的 10 种细分里出现了 8 种,没出现的是 HTTP/2 客户端专用的 InvalidConnectWithBody 和 ffi 专用的 AbortedByCallback。
legacy Client 再包一层
hyper-util 的 legacy Client 有自己的 Error,结构和 hyper::Error 相似:私有的 ErrorKind(Canceled、ChannelClosed、Connect、UserUnsupportedRequestMethod、UserUnsupportedVersion、UserAbsoluteUriRequired、SendRequest)加一个 source,外加出错连接的 connect_info。公开的判断方法只有 is_connect。它的 Display 更简单:
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "client error ({:?})", self.kind)
}
}
实验的最后一段:
--- hyper_util::client::legacy::Client
connection refused client error (Connect) -> tcp connect error -> Connection refused (os error 61)
is_connect: true
closed before response client error (SendRequest) -> connection closed before message completed
is_connect: false, source is hyper::Error: true
连不上时是 Connect,source 是 HttpConnector 的错误(见 HttpConnector);连上之后出的错是 SendRequest,source 就是 hyper 的 Error。所以拿到 legacy Client 的错误,要判断「是不是服务端关了连接」,得先 source() 再 downcast_ref::<hyper::Error>(),然后调 is_incomplete_message()。这里向下转型的是 hyper-util 自己往 source 里放的公开类型,和 hyper 文档说的「source 里的类型不稳定」不是一回事,但同样没有写进 hyper-util 的文档承诺。legacy Client 什么情况下自动重试,见 legacy Client。
Err,客户端收到什么?上面的程序里有两个用例被跳过了:/svc-err 的服务直接返回 Err("boom");/body-err 返回一个响应,body 先给一块 a,第二块是 Err("bad chunk")。两条连接各以什么 hyper::Error 结束?客户端在线上能读到 HTTP/1.1 500 吗?
动脑筋答案
这两段输出夹在「连接层面的错误」和「用户代码的错误」两段之间,8 次运行相同:
service Err error from user's Service -> boom
is_*: user
wire: (nothing)
body Err error from user's Body stream -> bad chunk
is_*: user
wire: (nothing)
User(Service):服务返回的错误被包成error from user's Service,原来的boom在 source 里。客户端什么都没收到:没有 500,连接直接关闭。hyper 的Service返回Err的意思是「这条连接不要了」,Dispatcher的poll_write拿到poll_msg的结果后,msg.map_err(crate::Error::new_user_service)?把错误一路传出去,Connection以错误结束。要给客户端一个 500,服务就得自己返回Ok(500 响应);axum 的处理函数不能返回Err给 hyper(它的Service::Error是Infallible),就是这个原因。User(Body):Dispatcher先把响应头编码进写缓冲,再 poll body;body 的第二帧出错,poll_write带着error from user's Body stream返回,还没来得及 flush,连接就结束了,客户端同样一个字节都没读到。
小结
hyper::Error是Box<ErrorImpl { kind, cause }>,一个指针大;Kind有 12 个变体,私有,大多带cfg。- 对外只有 11 个
is_*;Io、Body、BodyWrite、Http2、UnexpectedMessage没有对应的方法。is_timeout同时认HeaderTimeout和 source 链上的私有哨兵TimedOut(HTTP/2 keep-alive 超时);is_shutdown是 1.7.0 加的。 Display只打印这一层的固定文本(1.0 起,#3312),措辞不稳定;完整原因要沿source()走;从source()向下转型不属于稳定 API(#3318)。Debug会显示私有的Kind。- 实测每种 Kind 的原文都在上面;服务端的解析错误回 400/414/431,1xx 状态码被换成 500,其余错误(服务
Err、body 出错、读头超时、HTTP/2 前言)都是直接断开,不回任何响应。 - legacy
Client的错误是client error (Kind)加 source,is_connect之外的判断要downcast_ref::<hyper::Error>()后再用is_*。