解析与编码
主线位置:服务端主线的两端各有一步在这里。「字节 → 请求」时,
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,两个角色
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::Server | role::Client | |
|---|---|---|
Incoming(读进来的起始行) | RequestLine(方法 + URI) | StatusCode |
Outgoing(写出去的起始行) | StatusCode | RequestLine |
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 接受的形状,这里必须都认,否则头明明收全了,快速路径却一直说「没收全」。
\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(见下面的实验)。
问: 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 - 2 | checked_new 返回 Parse::TooLarge(见本页的动脑筋) |
Transfer-Encoding 最后一个编码是 chunked | CHUNKED |
Transfer-Encoding 最后一个不是 chunked | 400:请求的长度无法确定 |
HTTP/1.0 带 Transfer-Encoding | 400 |
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 那一项能看到这三点。
#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 |
Empty | content-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。
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 改的是这里,不是服务端。