Rust in Everything
基于 hyper 1.12.0

解析与编码

主线位置:服务端主线的两端各有一步在这里。「字节 → 请求」时,Server::parse 把读缓冲里的请求头解析成 Request 的头部,并定出 body 怎么读;「响应 → 字节」时,Server::encode 把响应头写进写缓冲,并定出 body 怎么写。客户端主线反过来,用 Client::encode 写请求头、Client::parse 读响应头。body 本身的字节由 body 的解码与编码 处理,缓冲区由 读写缓冲 管。

flowchart TB
  rbuf["read_buf (BytesMut)"] --> fast{"is_complete_fast"}
  fast -->|头还没收全| more[继续读 socket]
  fast -->|可能收全| sparse["Server::parse"]
  sparse -->|httparse 出错| onerr["on_error: 400/414/431"]
  sparse --> pm["ParsedMessage"]
  pm --> dec["Conn 选 Decoder"]
  pm --> svc["Request 交给 Service"]
  svc --> res["Response"]
  res --> blen["size_hint 定 BodyLength"]
  blen --> senc["Server::encode"]
  senc --> hbuf["写缓冲里的头部字节"]
  senc --> enc["Encoder"]

HTTP/1 的消息头是文本,body 的边界由消息头决定。所以「解析」和「编码」这两步除了在字节和 http::HeaderMap 之间转换,还要回答同一个问题:body 有多长,怎么分界。解析一侧的答案是一个 DecodedLength,交给 Conn 去建 Decoder;编码一侧的答案是一个 Encoder,交给 Conn 去写 body。

Http1Transaction:一个 trait,两个角色

Http1Transaction:

pub(crate) trait Http1Transaction {
    type Incoming;
    type Outgoing: Default;
    #[cfg(feature = "tracing")]
    const LOG: &'static str;
    fn parse(bytes: &mut BytesMut, ctx: ParseContext<'_>) -> ParseResult<Self::Incoming>;
    fn encode(enc: Encode<'_, Self::Outgoing>, dst: &mut Vec<u8>) -> crate::Result<Encoder>;

    fn on_error(err: &crate::Error) -> Option<MessageHead<Self::Outgoing>>;

    fn is_client() -> bool {
        !Self::is_server()
    }

    fn is_server() -> bool {
        !Self::is_client()
    }

    fn should_error_on_parse_eof() -> bool {
        Self::is_client()
    }

    fn should_read_first() -> bool {
        Self::is_server()
    }

    fn update_date() {}
}

两个实现都是没有值的空枚举:Server 和 Client。它们只是类型层面的开关,Conn<I, B, T> 和 Dispatcher 对 T 泛型,编译出两份代码。

role::Serverrole::Client
Incoming(读进来的起始行)RequestLine(方法 + URI)StatusCode
Outgoing(写出去的起始行)StatusCodeRequestLine
on_error能回一个 4xx 就回总是 None,没法告诉服务端
should_read_first是:先读请求否:先写请求
update_date刷新 Date 缓存空

parse 返回的 ParsedMessage 除了消息头,还带着四个结论:

pub(crate) struct ParsedMessage<T> {
    head: MessageHead<T>,
    decode: DecodedLength,
    expect_continue: bool,
    keep_alive: bool,
    wants_upgrade: bool,
}

decode 是 body 长度;keep_alive 是这条消息允不允许连接复用;expect_continue 和 wants_upgrade 交给 Conn 的读写状态机 和 协议升级。Conn::poll_read_head 拿到它之后据此进入 Reading::Body、Reading::Continue 或直接 Reading::KeepAlive。

解析请求头

头没收全时只做快速扫描

Conn 读一次 socket 就调一次 parse_headers。慢速客户端可能一个字节一个字节地发,每次都用 httparse 从头解析一遍很浪费。所以上一次没解析完时(prev_len 是上次的缓冲长度),先用 is_complete_fast 在新到的字节里找头部结束标记,没找到就直接返回 Ok(None):

fn is_complete_fast(bytes: &[u8], prev_len: usize) -> bool {
    let start = prev_len.saturating_sub(3);
    let bytes = &bytes[start..];

    for (i, b) in bytes.iter().copied().enumerate() {
        if b == b'\r' {
            if bytes[i + 1..].chunks(3).next() == Some(&b"\n\r\n"[..]) {
                return true;
            }
        } else if b == b'\n'
            && (bytes.get(i + 1) == Some(&b'\n')
                || bytes[i + 1..].chunks(2).next() == Some(&b"\r\n"[..]))
        {
            return true;
        }
    }

    false
}

从 prev_len - 3 开始,是因为结束标记最长 4 个字节,可能有 3 个字节在上一次读到的末尾。扫描只认三种结束形状:\r\n\r\n、\n\n、\n\r\n。httparse 接受的形状,这里必须都认,否则头明明收全了,快速路径却一直说「没收全」。

📝NOTE
冷知识:\n\r\n 是 1.11.1 才补上的

#4147(修 issue #4145,首次发布于 v1.11.1)之前,上面的 \n 分支只认 \n\n。提交说明写得很清楚:「is_complete_fast recognizes \r\n\r\n and \n\n as head terminators but not \n\r\n, while the full parser (httparse) accepts all three. So a request whose head ends with \n\r\n parses fine when it arrives in a single read, but stalls when it arrives split across reads」。同一个提交在 test_is_complete_fast 里加了 \n\r\n 的正例和 \n\r 的反例,又加了一个 test_parse_accepts_lf_crlf_terminator,证明完整解析器接受这种结尾。快速路径是一个优化,它和完整解析器对「什么算结束」的判断必须一致,这个 bug 就是两边不一致的结果。

Server::parse:栈上的头数组

Server::parse 的第一段准备 httparse 要的头数组:

        // Both headers_indices and headers are using uninitialized memory,
        // but we *never* read any of it until after httparse has assigned
        // values into it. By not zeroing out the stack memory, this saves
        // a good ~5% on pipeline benchmarks.
        let mut headers_indices: SmallVec<[MaybeUninit<HeaderIndices>; DEFAULT_MAX_HEADERS]> =
            match ctx.h1_max_headers {
                Some(cap) => smallvec![MaybeUninit::uninit(); cap],
                None => smallvec_inline![MaybeUninit::uninit(); DEFAULT_MAX_HEADERS],
            };
        {
            let mut headers: SmallVec<[MaybeUninit<httparse::Header<'_>>; DEFAULT_MAX_HEADERS]> =
                match ctx.h1_max_headers {
                    Some(cap) => smallvec![MaybeUninit::uninit(); cap],
                    None => smallvec_inline![MaybeUninit::uninit(); DEFAULT_MAX_HEADERS],
                };

DEFAULT_MAX_HEADERS 是 100。两个数组都是 SmallVec,内联容量 100,元素是 MaybeUninit,不清零。httparse 的 parse_request_with_uninit_headers 只往里写,hyper 只读 httparse 写过的前 headers_len 个,这就是注释里说的省下约 5%。

头的个数超过数组长度,httparse 返回 TooManyHeaders,被 From<httparse::Error> 转成 Parse::TooLarge,服务端回 431(见下面的实验)。

📝NOTE
没有蠢问题

问: Builder::max_headers 的文档说,设了这个值以后「headers will be allocated in heap memory」,每个请求都会有一次堆分配。那我设 max_headers(50) 想省点内存,反而变慢了?

答: 不会。设了值走的是 smallvec![MaybeUninit::uninit(); cap],这个宏调 SmallVec::from_elem,smallvec 1.16.3 里它只在 n > Self::inline_capacity() 时才 vec![elem; n] 上堆,否则仍然放在内联数组里。内联容量固定是 DEFAULT_MAX_HEADERS,所以 cap <= 100 还在栈上,只有 cap > 100 才上堆。文档的说法只对调大的情况成立。调小也不省栈空间:数组大小在编译期就定成 100 了。

httparse 解析出来的是指向 buf 的 &str / &[u8],它们借用着 buf,不能活过这个块。record_header_indices 把每个头的名字和值换算成相对 buf 起点的下标存进 headers_indices。块结束后:

        let slice = buf.split_to(len).freeze();

请求头那一段从读缓冲里切下来,变成一个 Bytes,之后每个头的值都是 slice.slice(start..end),URI 也是 slice.slice_ref(...)。它们共享同一块内存,不拷贝。头名还要经过 HeaderName::from_bytes,常见的标准头名会变成静态常量,不分配。

解析完成后还有两道长度检查:配了 max_header_size 时,整个头部(含起始行)超过它就是 Parse::TooLarge;URI 超过 MAX_URI_LEN(u16::MAX - 1,即 65534 字节)是 Parse::UriTooLong。版本号只有 HTTP/1.1 时 keep_alive 默认为真,HTTP/1.0 默认为假。

决定 body 长度

接下来逐个把头放进 HeaderMap,顺便看五种头。注释列出了 RFC 7230 §3.3.3 的规则里和请求相关的几条:

        // According to https://tools.ietf.org/html/rfc7230#section-3.3.3
        // 1. (irrelevant to Request)
        // 2. (irrelevant to Request)
        // 3. Transfer-Encoding: chunked has a chunked body.
        // 4. If multiple differing Content-Length headers or invalid, close connection.
        // 5. Content-Length header has a sized body.
        // 6. Length 0.
        // 7. (irrelevant to Request)

处理 Transfer-Encoding 和 Content-Length 的两个分支:

                header::TRANSFER_ENCODING => {
                    // ...
                    if !is_http_11 {
                        debug!("HTTP/1.0 cannot have Transfer-Encoding header");
                        return Err(Parse::transfer_encoding_unexpected());
                    }
                    is_te = true;
                    if is_cl && con_len.take().is_some() {
                        headers.remove(header::CONTENT_LENGTH);
                    }
                    if headers::is_chunked_(&value) {
                        is_te_chunked = true;
                        decoder = DecodedLength::CHUNKED;
                    } else {
                        is_te_chunked = false;
                    }
                }
                header::CONTENT_LENGTH => {
                    is_cl = true;
                    if is_te {
                        continue;
                    }
                    let len = headers::content_length_parse(&value)
                        .ok_or_else(Parse::content_length_invalid)?;
                    if let Some(prev) = con_len {
                        if prev != len {
                            // ...
                            return Err(Parse::content_length_invalid());
                        }
                        // we don't need to append this secondary length
                        continue;
                    }
                    decoder = DecodedLength::checked_new(len)?;
                    con_len = Some(len);
                }

循环结束后还有两条:

        if is_te && !is_te_chunked {
            debug!("request with transfer-encoding header, but not chunked, bad request");
            return Err(Parse::transfer_encoding_invalid());
        }

        if is_te && is_cl {
            keep_alive = false;
        }

合起来,请求 body 的长度这样定:

请求头结果
都没有DecodedLength::ZERO,没有 body
只有 Content-Length: n长度 n;多个值必须相同,否则 400
Content-Length 不是纯数字(含 +3、空串)400,from_digits 只认 0-9
Content-Length 大于 u64::MAX - 2checked_new 返回 Parse::TooLarge(见本页的动脑筋)
Transfer-Encoding 最后一个编码是 chunkedCHUNKED
Transfer-Encoding 最后一个不是 chunked400:请求的长度无法确定
HTTP/1.0 带 Transfer-Encoding400
Transfer-Encoding 和 Content-Length 都有按 chunked 读,删掉 Content-Length,这个请求之后关连接

Content-Length 只用 content_length_parse 看单个值,所以 content-length: 3, 3 这种一行里写两个值的会被当成非法;两行相同的值可以,第二行不放进 HeaderMap。is_chunked_ 只看逗号分隔的最后一项,大小写不敏感。

其余三个头:Connection 决定 keep_alive(一旦出现 close,后面的 keep-alive 不能把它翻回来,这是 1.12.0 的 #4201,见 Conn 的读写状态机);Expect: 100-continue 置 expect_continue;Upgrade 只在 HTTP/1.1 下置 wants_upgrade,CONNECT 方法一开始就置上。

CL 和 TE 同时出现是请求走私(request smuggling)的经典入口:前面的代理按 Content-Length 分界,后面的服务器按 chunked 分界,两边对「这个请求到哪结束」的理解不同,多出来的字节就成了下一个请求。hyper 按 chunked 读,删掉 Content-Length,不让它被转发出去,并且回完这个响应就关连接,让「下一个请求」不存在。下面的实验里 cl then te 那一项能看到这三点。

📝NOTE
冷知识:删掉 Content-Length 和关连接都是 1.11.0 才有的

#4124(修 issue #4123,首次发布于 v1.11.0)之前,TE 分支里没有 if is_cl && con_len.take().is_some() 那一句,循环后也没有 if is_te && is_cl { keep_alive = false; }。issue 指出两个问题:一是「a Content-Length seen after Transfer-Encoding is skipped by the header loop, but a Content-Length seen before it is appended to the header map and survives」,Content-Length 留不留在 HeaderMap 里取决于两个头在线上的顺序;二是 RFC 9112 §6.1 要求「the server MUST close the connection after responding to such a request」,而 hyper 没有关。

PR 的说明强调 body 的分界一直是对的:「To be clear, the semantics have always been correct. This reduces the possibility of forwarding confusing headers to a remote that doesn't know how to act correctly.」修复为 CONTENT_LENGTH 分支加了 is_cl 标记,TE 分支据此删掉已经放进去的 Content-Length。

解析失败时回什么

解析出错时,Conn::on_parse_error 先看响应头还没开始写(Writing::Init),再看读缓冲开头是不是 HTTP/2 的连接前言,都不是就问 Server::on_error 要一个响应头:

    fn on_error(err: &crate::Error) -> Option<MessageHead<Self::Outgoing>> {
        use crate::error::Kind;
        let status = match *err.kind() {
            Kind::Parse(Parse::Method)
            | Kind::Parse(Parse::Header(_))
            | Kind::Parse(Parse::Uri)
            | Kind::Parse(Parse::Version) => StatusCode::BAD_REQUEST,
            Kind::Parse(Parse::TooLarge) => StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
            Kind::Parse(Parse::UriTooLong) => StatusCode::URI_TOO_LONG,
            _ => return None,
        };

只有三个状态码:400、414、431。没有 505:版本号不对(HTTP/1.2)是 Parse::Version,回的是 400。Parse::Header(_) 包括头名或值里的非法字符,也包括上一节的 Content-Length / Transfer-Encoding 错误。Parse::TooLarge 有五个来源:头超过 max_headers 个、头部字节超过 max_header_size、读缓冲涨到 max_buf_size 还没收全(见读写缓冲)、单个头名不短于 64 KiB(record_header_indices 里的 1 << 16)、Content-Length 超过 u64::MAX - 2。

回错误响应时 body 是 None,所以带 content-length: 0;解析出错时 Conn 已经 close_read,keep-alive 关掉了,enforce_version 会补一个 connection: close。写完这个响应,连接关闭,serve_connection 返回原来那个解析错误。连接前言 PRI * HTTP/2.0 则不回任何响应,直接返回 invalid HTTP version parsed (found HTTP2 preface),让 hyper-util 的 auto(见 auto:一个端口两个协议)之类的上层知道这是 HTTP/2。

实验:线上的字节

用一个裸 TcpStream 对 hyper 服务端发手写的字节,原样打印回来的东西(\r\n 显示成 \r\n 再换行)。读到 EOF 打印 [EOF],300ms 没有新数据打印 [still open],被 RST 打印 [reset];然后打印 serve_connection 返回的结果。

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["rt", "net", "macros", "io-util", "time", "sync"] }

客户端一侧的辅助函数(src/common.rs):

use std::error::Error as _;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::time::timeout;

pub fn show(bytes: &[u8]) -> String {
  let s = String::from_utf8_lossy(bytes);
  s.replace("\r\n", "\\r\\n\n")
}

pub fn err_chain(e: &hyper::Error) -> String {
  let mut s = e.to_string();
  let mut src = e.source();
  while let Some(inner) = src {
    s.push_str(&format!(": {inner}"));
    src = inner.source();
  }
  s
}

// Send `req`, read until EOF or 300ms of silence, print it.
pub async fn exchange(addr: std::net::SocketAddr, name: &str, req: &[u8], rx: &mut mpsc::UnboundedReceiver<String>) {
  println!("== {name}");
  let mut tcp = TcpStream::connect(addr).await.unwrap();
  if let Err(e) = tcp.write_all(req).await { println!("(client write error: {e})"); }
  let mut got = Vec::new();
  let mut buf = vec![0u8; 64 * 1024];
  let end = loop {
    match timeout(Duration::from_millis(300), tcp.read(&mut buf)).await {
      Ok(Ok(0)) => break "[EOF]",
      Ok(Ok(n)) => got.extend_from_slice(&buf[..n]),
      Ok(Err(_)) => break "[reset]",
      Err(_) => break "[still open]",
    }
  };
  print!("{}", show(&got));
  if !got.ends_with(b"\r\n") && !got.is_empty() {
    println!();
  }
  println!("{end}");
  drop(tcp);
  match timeout(Duration::from_secs(1), rx.recv()).await {
    Ok(Some(s)) => println!("server: {s}"),
    _ => println!("server: (no result)"),
  }
}

服务端和用例(src/bin/wire.rs):

#[path = "../common.rs"]
mod common;

use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full, StreamBody};
use hyper::body::{Body, Frame, Incoming, SizeHint};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::sync::mpsc;

// Claims 5 bytes, delivers 3.
struct Short(bool);

impl Body for Short {
  type Data = Bytes;
  type Error = Infallible;
  fn poll_frame(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
    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)
  }
}

type B = BoxBody<Bytes, Infallible>;

async fn handle(req: Request<Incoming>) -> Result<Response<B>, Infallible> {
  let path = req.uri().path().to_string();
  let res = match path.as_str() {
    "/full" => Response::new(Full::new(Bytes::from("hello")).boxed()),
    "/empty" => Response::new(Empty::new().boxed()),
    "/stream" => {
      let frames = vec![
        Ok::<_, Infallible>(Frame::data(Bytes::from("abc"))),
        Ok(Frame::data(Bytes::from("defg"))),
      ];
      Response::new(StreamBody::new(futures::stream::iter(frames)).boxed())
    }
    "/nocontent" => Response::builder()
      .status(StatusCode::NO_CONTENT)
      .body(Empty::new().boxed())
      .unwrap(),
    "/short" => Response::new(Short(false).boxed()),
    "/echo" => {
      let cl = req.headers().get("content-length").cloned();
      let te = req.headers().get("transfer-encoding").cloned();
      let body = req.into_body().collect().await.map(|c| c.to_bytes());
      let text = format!("cl={cl:?} te={te:?} body={body:?}");
      Response::new(Full::new(Bytes::from(text)).boxed())
    }
    _ => Response::new(Empty::new().boxed()),
  };
  Ok(res)
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let (tx, mut rx) = mpsc::unbounded_channel();
  let addr = spawn_server(false, tx.clone()).await;
  let title_addr = spawn_server(true, tx).await;

  let long_uri = format!("GET /{} HTTP/1.1\r\n\r\n", "a".repeat(65534));
  let many: String = (0..101).map(|i| format!("x-h{i}: v\r\n")).collect();
  let many_headers = format!("GET / HTTP/1.1\r\n{many}\r\n");
  let huge = format!("GET / HTTP/1.1\r\nx-big: {}\r\n\r\n", "a".repeat(500 * 1024));

  let cases: Vec<(&str, Vec<u8>)> = vec![
    ("full", b"GET /full HTTP/1.1\r\n\r\n".to_vec()),
    ("stream", b"GET /stream HTTP/1.1\r\n\r\n".to_vec()),
    ("empty", b"GET /empty HTTP/1.1\r\n\r\n".to_vec()),
    ("head full", b"HEAD /full HTTP/1.1\r\n\r\n".to_vec()),
    ("204", b"GET /nocontent HTTP/1.1\r\n\r\n".to_vec()),
    ("stream over 1.0", b"GET /stream HTTP/1.0\r\n\r\n".to_vec()),
    ("short body", b"GET /short HTTP/1.1\r\n\r\n".to_vec()),
    (
      "cl then te",
      b"POST /echo HTTP/1.1\r\ncontent-length: 3\r\ntransfer-encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n".to_vec(),
    ),
    ("te gzip", b"POST /echo HTTP/1.1\r\ntransfer-encoding: gzip\r\n\r\n".to_vec()),
    ("te on 1.0", b"POST /echo HTTP/1.0\r\ntransfer-encoding: chunked\r\n\r\n".to_vec()),
    ("cl abc", b"POST /echo HTTP/1.1\r\ncontent-length: abc\r\n\r\n".to_vec()),
    ("cl 3 and 4", b"POST /echo HTTP/1.1\r\ncontent-length: 3\r\ncontent-length: 4\r\n\r\n".to_vec()),
    ("cl u64::MAX", b"POST /echo HTTP/1.1\r\ncontent-length: 18446744073709551615\r\n\r\n".to_vec()),
    ("bad method", b"G@T / HTTP/1.1\r\n\r\n".to_vec()),
    ("bad version", b"GET / HTTP/1.2\r\n\r\n".to_vec()),
    ("bad header", b"GET / HTTP/1.1\r\nbad header: x\r\n\r\n".to_vec()),
    ("uri 65535", long_uri.into_bytes()),
    ("101 headers", many_headers.into_bytes()),
    ("500KiB head", huge.into_bytes()),
    ("h2 preface", b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".to_vec()),
  ];
  for (name, req) in cases {
    common::exchange(addr, name, &req, &mut rx).await;
  }
  common::exchange(title_addr, "full, title_case_headers(true)", b"GET /full HTTP/1.1\r\n\r\n", &mut rx).await;
}

async fn spawn_server(title_case: bool, tx: mpsc::UnboundedSender<String>) -> std::net::SocketAddr {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(async move {
    loop {
      let (stream, _) = listener.accept().await.unwrap();
      let tx = tx.clone();
      tokio::spawn(async move {
        let r = http1::Builder::new()
          .title_case_headers(title_case)
          .serve_connection(TokioIo::new(stream), service_fn(handle))
          .await;
        let _ = tx.send(match r {
          Ok(()) => "Ok".to_string(),
          Err(e) => format!("Err({})", common::err_chain(&e)),
        });
      });
    }
  });
  addr
}

M2 Max、rustc 1.99.0、--release。先看请求头出错的几项:

== cl then te
HTTP/1.1 200 OK\r\n
connection: close\r\n
content-length: 42\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
cl=None te=Some("chunked") body=Ok(b"abc")
[EOF]
server: Ok
== te gzip
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid transfer-encoding parsed)
== te on 1.0
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(unexpected transfer-encoding parsed)
== cl abc
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid content-length parsed)
== cl 3 and 4
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid content-length parsed)
== bad method
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid HTTP method parsed)
== bad version
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid HTTP version parsed)
== bad header
HTTP/1.1 400 Bad Request\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(invalid HTTP header parsed)
== uri 65535
HTTP/1.1 414 URI Too Long\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(URI too long)
== 101 headers
HTTP/1.1 431 Request Header Fields Too Large\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(message head is too large)
== 500KiB head
HTTP/1.1 431 Request Header Fields Too Large\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[reset]
server: Err(message head is too large)
== h2 preface
[EOF]
server: Err(invalid HTTP version parsed (found HTTP2 preface))

连跑 3 次,除了 date 的值,输出相同。更早的几次运行里,500KiB head 的最后一行有时是 [EOF]、有时是 [reset]:服务端回完 431 就关 socket,此时客户端写的 500 KiB 还有一部分没被读走,内核关闭一个接收缓冲非空的 socket 会发 RST,客户端先读到 431 还是先收到 RST 取决于时机。431 本身每次都在。

  • cl then te:处理函数看到的 Content-Length 是 None,body 按 chunked 读出 abc;响应带 connection: close,之后连接关闭。
  • 「101 个头」超过默认的 100,回 431;65535 字节的 URI 比 MAX_URI_LEN 多 1,回 414。
  • 500KiB head 超过默认 max_buf_size(约 408 KiB),回 431,max_buf_size 的检查在读写缓冲里讲。

编码响应头

Server::encode 先处理几种特殊的状态码:

        let (ret, is_last) = if msg.head.subject == StatusCode::SWITCHING_PROTOCOLS {
            (Ok(()), true)
        } else if msg.req_method == &Some(Method::CONNECT) && msg.head.subject.is_success() {
            // Sending content-length or transfer-encoding header on 2xx response
            // to CONNECT is forbidden in RFC 7231.
            wrote_len = true;
            (Ok(()), true)
        } else if msg.head.subject.is_informational() {
            warn!("response with 1xx status code not supported");
            *msg.head = MessageHead::default();
            msg.head.subject = StatusCode::INTERNAL_SERVER_ERROR;
            msg.body = None;
            (Err(crate::Error::new_user_unsupported_status_code()), true)
        } else {
            (Ok(()), !msg.keep_alive)
        };

is_last 表示这条响应写完后连接是否结束:101 和 CONNECT 的 2xx 之后连接要交给升级,keep-alive 已关的也是最后一条。Service 返回 1xx 是用户错误:头部被换成默认值加 500,body 丢掉,encode 返回错误(100 Continue 由 Conn 自己在 Reading::Continue 里发,不经过这里)。把实验程序的处理函数换成返回 103 的:

async fn handle(_: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
  Ok(Response::builder().status(StatusCode::from_u16(103).unwrap()).body(Full::new(Bytes::from("x"))).unwrap())
}

客户端收到 500,serve_connection 以错误结束:

== 103 from service
HTTP/1.1 500 Internal Server Error\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:17:24 GMT\r\n
\r\n
[EOF]
server: Err(response has 1xx status code, not supported by server)

起始行有一条快速路径:HTTP/1.1、200、没有自定义原因短语时,直接写常量 HTTP/1.1 200 OK\r\n。其他情况拼版本、状态码和 canonical_reason(),没有标准原因短语的状态码写 <none>,注释说「a reason MUST be written, as many parsers will expect it」。

Content-Length 还是 chunked

头部的主体在 encode_headers。它用 drain() 遍历响应的 HeaderMap,对 Content-Length、Transfer-Encoding、Connection、Date、Trailer 特殊处理,其余照写。用户自己写了 Content-Length 或 Transfer-Encoding 时以用户的为准(wrote_len = true);都没写时,看 msg.body:

        if !wrote_len {
            encoder = match msg.body {
                Some(BodyLength::Unknown) => {
                    if msg.head.version == Version::HTTP_10
                        || !Server::can_chunked(msg.req_method.as_ref(), msg.head.subject)
                    {
                        Encoder::close_delimited()
                    } else {
                        header_name_writer.write_full_header_line(
                            dst,
                            "transfer-encoding: chunked\r\n",
                            (header::TRANSFER_ENCODING, ": chunked\r\n"),
                        );
                        Encoder::chunked()
                    }
                }
                None | Some(BodyLength::Known(0)) => {
                    if Server::can_have_implicit_zero_content_length(
                        msg.req_method.as_ref(),
                        msg.head.subject,
                    ) {
                        header_name_writer.write_full_header_line(
                            dst,
                            "content-length: 0\r\n",
                            (header::CONTENT_LENGTH, ": 0\r\n"),
                        );
                    }
                    Encoder::length(0)
                }
                Some(BodyLength::Known(len)) => {
                    if !Server::can_have_content_length(msg.req_method.as_ref(), msg.head.subject) {
                        Encoder::length(0)
                    } else {
                        header_name_writer.write_header_name_with_colon(
                            dst,
                            "content-length: ",
                            header::CONTENT_LENGTH,
                        );
                        extend(dst, ::itoa::Buffer::new().format(len).as_bytes());
                        extend(dst, b"\r\n");
                        Encoder::length(len)
                    }
                }
            };
        }

msg.body 是 Dispatcher 在 poll_write 里根据响应 body 算出来的:is_end_stream() 为真是 None,size_hint().exact() 有值是 Known(n),否则是 Unknown。http_body 的这两个方法见 Service 与 Body。于是:

响应 body线上
Full::new("hello")content-length: 5
Emptycontent-length: 0
StreamBody(不知道长度)transfer-encoding: chunked
不知道长度,但请求是 HTTP/1.0两个头都不写,写完关连接(close-delimited)
HEAD 请求照常写 content-length,但 body 强制为 0 字节
204 / 304两个头都不写,body 强制为 0 字节

最后两行来自 can_chunked 和 can_have_content_length:HEAD 不能有 body 但可以有 Content-Length(告诉客户端 GET 会拿到多长),1xx、204、304 和 CONNECT 的 2xx 两样都不能有。循环结束前还有一句兜底:can_have_body 为假时,不管前面选了什么,encoder = Encoder::length(0)。

同一个实验的另几项:

== full
HTTP/1.1 200 OK\r\n
content-length: 5\r\n
date: Fri, 23 Oct 2026 13:13:14 GMT\r\n
\r\n
hello
[still open]
server: Ok
== stream
HTTP/1.1 200 OK\r\n
transfer-encoding: chunked\r\n
date: Fri, 23 Oct 2026 13:13:15 GMT\r\n
\r\n
3\r\n
abc\r\n
4\r\n
defg\r\n
0\r\n
\r\n
[still open]
server: Ok
== empty
HTTP/1.1 200 OK\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:15 GMT\r\n
\r\n
[still open]
server: Ok
== head full
HTTP/1.1 200 OK\r\n
content-length: 5\r\n
date: Fri, 23 Oct 2026 13:13:15 GMT\r\n
\r\n
[still open]
server: Ok
== 204
HTTP/1.1 204 No Content\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[still open]
server: Ok
== stream over 1.0
HTTP/1.0 200 OK\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
abcdefg
[EOF]
server: Ok
  • head full:响应头里是 content-length: 5,后面没有 body,连接保持。
  • stream over 1.0:请求是 HTTP/1.0,enforce_version 把响应的版本也降成 HTTP/1.0;HTTP/1.0 没有 chunked,长度又未知,只能写完关连接,客户端靠 EOF 知道 body 结束。
  • [still open] 的几项是 HTTP/1.1 默认 keep-alive,连接在等下一个请求。

chunked 的帧格式、short body 一项为什么客户端什么都没收到,见 body 的解码与编码。

用户写了 Content-Length 时,hyper 不核对它和 body 的实际长度是否一致:Known 时注释说「For performance reasons, we are just going to trust that the values match」,只在 debug 构建里 assert!。用户写了不以 chunked 结尾的 Transfer-Encoding(比如 gzip),must_write_chunked 会在行尾补 , chunked。

Date:每秒格式化一次

encode_headers 的最后:

        // cached date is much faster than formatting every request
        // don't force the write if disabled
        if !wrote_date && msg.date_header {
            dst.reserve(date::DATE_VALUE_LENGTH + 8);
            header_name_writer.write_header_name_with_colon(dst, "date: ", header::DATE);
            date::extend(dst);
            extend(dst, b"\r\n\r\n");
        } else {
            extend(dst, b"\r\n");
        }

用户自己设了 Date 就不再写;auto_date_header(false) 可以关掉,默认开。值来自 common/date.rs 里一个线程局部的缓存:

thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate::new()));

CachedDate 存着格式化好的 29 个字节(DATE_VALUE_LENGTH,"Sun, 06 Nov 1994 08:49:37 GMT".len())和下一次该刷新的时刻。刷新在 update 里:

    fn update(&mut self, now: SystemTime) {
        let nanos = now
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .subsec_nanos();

        self.render(now);
        self.next_update = now + Duration::new(1, 0) - Duration::from_nanos(u64::from(nanos));
    }

next_update 对齐到下一个整秒。谁来调用?Server::update_date 调 date::update(),后者调 check():当前时间过了 next_update 才重新格式化。Dispatcher::poll_inner 每次被 poll 都先调 T::update_date(),所以一个线程上不管有多少连接,每秒最多格式化一次,其余时候 date::extend 只是拷 29 个字节。缓存是线程局部的,不需要锁;多线程运行时里每个工作线程各有一份。

头名的大小写

HeaderMap 里的头名一律是小写,hyper 默认就按小写写出去(LowercaseWriter)。HTTP/1 的头名本来大小写不敏感,但总有对端只认 Content-Length 这种写法,所以有两个选项:

  • title_case_headers(true):写出时把首字母和每个 - 后面的字母改成大写,见 title_case。
  • preserve_header_case(true):解析时把每个头在线上的原始写法记进一个 HeaderCaseMap,作为扩展放在 Request 上;编码时如果 Response 的扩展里有 HeaderCaseMap,就按里面的原始写法写。HeaderCaseMap 是私有类型,文档承认「there is no way to interact with the original cases」,它唯一的用途是代理:把收到的请求原样转发时,大小写也跟着走。

这两个选项都走 encode_headers_with_original_case,它标了 #[cold] 和 #[inline(never)],不内联进常见的小写路径。实验的最后一项开了 title_case_headers:

== full, title_case_headers(true)
HTTP/1.1 200 OK\r\n
Content-Length: 5\r\n
Date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
hello
[still open]
server: Ok

客户端方向

客户端用同一套机制,规则镜像过来。

Client::encode 写请求头前先调 set_length 决定请求 body 的写法。和服务端不同的是,它直接改 HeaderMap(插入 content-length 或 transfer-encoding: chunked),然后统一写出:

  • 没有 body:删掉 Transfer-Encoding,长度 0。
  • HTTP/1.0 请求:删掉 Transfer-Encoding;有 Content-Length 用它,长度已知就补上,否则不能带 body。
  • 用户写了 Transfer-Encoding:确保最后一项是 chunked(add_chunked 补上),并删掉 Content-Length。
  • 用户写了合法的 Content-Length:用它。
  • 长度未知:GET、HEAD、CONNECT 当作没有 body(注释:「GET, HEAD, and CONNECT almost never have bodies」),其余方法插入 transfer-encoding: chunked 并删掉不合法的 Content-Length。
  • 长度已知:插入 content-length。

1.12.0 的 #4223 和 #4224 改的就是这里:前者把「用户写了 Transfer-Encoding」时删 Content-Length 的条件从 should_remove_con_len && existing_con_len.is_some() 改成 should_remove_con_len,不合法的 Content-Length 也删;后者在自动加 chunked 的分支里补了 should_remove_con_len = true。两个改动都在 Client::set_length,服务端编码响应的逻辑没变。

Client::decoder 定响应 body 的长度,比服务端多几条:HEAD 的响应、1xx、204、304 没有 body;CONNECT 的 2xx 和 101 是升级;Transfer-Encoding 不以 chunked 结尾、或两个头都没有时,读到连接关闭为止(CLOSE_DELIMITED),这对请求是 400,对响应却是合法的。Client::parse 还在一个循环里跳过 100、102..=199 这些中间响应,交给 on_informational 回调(如果设了)。Client::on_error 永远返回 None:服务端发来的东西解析不了,客户端没有地方回 400。

❗IMPORTANT
动脑筋:content-length: 18446744073709551615 回什么?上面的实验里有一项:
    ("cl u64::MAX", b"POST /echo HTTP/1.1\r\ncontent-length: 18446744073709551615\r\n\r\n".to_vec()),

18446744073709551615 是 u64::MAX,from_digits 能解析,不溢出。hyper 回的状态码是多少?

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次,除了 date 的值输出相同:

== cl u64::MAX
HTTP/1.1 431 Request Header Fields Too Large\r\n
connection: close\r\n
content-length: 0\r\n
date: Fri, 23 Oct 2026 13:13:16 GMT\r\n
\r\n
[EOF]
server: Err(message head is too large)

是 431,不是 400。content_length_parse 解析成功,接着 DecodedLength::checked_new 检查上限:

    pub(crate) fn checked_new(len: u64) -> Result<Self, crate::error::Parse> {
        if len <= MAX_LEN {
            Ok(DecodedLength(len))
        } else {
            warn!("content-length bigger than maximum: {} > {}", len, MAX_LEN);
            Err(crate::error::Parse::TooLarge)
        }
    }

MAX_LEN 是 u64::MAX - 2。DecodedLength 只是一个 u64,最大的两个值被借来表示 CHUNKED(u64::MAX - 1)和 CLOSE_DELIMITED(u64::MAX),真实长度不能占用它们。超出时返回的是 Parse::TooLarge,而 on_error 把 TooLarge 一律映射成 431,所以一个「body 太长」的请求得到的是「请求头太大」的状态码。多一位数字 18446744073709551616 则在 from_digits 的 checked_mul / checked_add 里溢出,返回 None,是 invalid content-length parsed,回 400。

小结

  • Http1Transaction 有 role::Server 和 role::Client 两个实现,都是空枚举,只在类型上区分角色;parse 产出消息头加四个结论(body 长度、keep-alive、Expect、升级),encode 写出消息头并返回 Encoder。
  • 头没收全时 is_complete_fast 只在新字节里找结束标记,认 \r\n\r\n、\n\n、\n\r\n 三种(第三种是 1.11.1 补的)。
  • Server::parse 的两个头数组是内联容量 100 的 SmallVec<MaybeUninit<_>>,不清零;max_headers 设到 100 以内仍在栈上。头的值和 URI 都是同一个 Bytes 的切片,不拷贝。
  • 请求 body 长度:两个头都没有是 0;Transfer-Encoding 必须以 chunked 结尾,HTTP/1.0 不能带;Content-Length 只认纯数字,多个值必须相同;两者都有时按 chunked 读、删掉 Content-Length、回完就关连接(1.11.0 起)。
  • 解析失败只自动回 400、414、431 三种,没有 505;TooLarge 的来源包括头太多、头太长、Content-Length 超过 u64::MAX - 2。HTTP/2 前言不回响应。
  • 响应头:用户写了 Content-Length / Transfer-Encoding 以用户的为准;否则 is_end_stream 写 content-length: 0,size_hint 精确写 content-length,未知写 chunked,HTTP/1.0 下未知长度靠关连接分界。HEAD、204、304 的 body 强制为空。
  • Date 由线程局部缓存提供,Dispatcher 每次 poll 时检查,每秒最多格式化一次。
  • 客户端在 set_length 里直接改请求的 HeaderMap;1.12.0 的 #4223、#4224 改的是这里,不是服务端。

💬 关联讨论

发起讨论
加载中...