body 的解码与编码
主线位置:服务端主线上,
Decoder把读缓冲里的请求 body 字节切成一个个Frame,交给Service手里的Incoming;Encoder把响应 body 的每一帧加上分界(chunked 的块头、或者只是按长度截断),放进写缓冲。客户端主线上两者互换:Encoder写请求 body,Decoder读响应 body。用哪一种,由解析与编码里的消息头决定。
flowchart TB dl["DecodedLength"] --> dnew["Decoder::new"] dnew -->|CHUNKED| dch["Chunked 状态机"] dnew -->|CLOSE_DELIMITED| deof["Eof:读到关闭"] dnew -->|n| dlen["Length(n)"] dch --> fr["Frame:data 或 trailers"] deof --> fr dlen --> fr fr --> inc["Incoming"] body["响应 body 的帧"] --> enc["Encoder::encode"] enc --> ebuf["EncodedBuf"] ebuf --> wbuf["写缓冲"]
body 的字节和消息头不一样,不需要整个收齐再处理:读到一点就交出去一点。所以 Decoder 和 Encoder 都是增量的,Decoder::decode 每次返回一帧,Encoder::encode 每次包装一帧。它们只关心分界,不碰 IO:Decoder 通过 MemRead 从读写缓冲里取字节,Encoder 产出的 EncodedBuf 由 Conn 放进写缓冲。
Decoder:三种分界
enum Kind {
/// A Reader used when a Content-Length header is passed with a positive integer.
Length(u64),
/// A Reader used when Transfer-Encoding is `chunked`.
Chunked {
state: ChunkedState,
chunk_len: u64,
extensions_cnt: u64,
trailers_buf: Option<BytesMut>,
trailers_cnt: usize,
h1_max_headers: Option<usize>,
h1_max_header_size: Option<usize>,
},
/// A Reader used for responses that don't indicate a length or chunked.
///
/// The bool tracks when EOF is seen on the transport.
///
/// Note: This should only used for `Response`s. It is illegal for a
/// `Request` to be made with both `Content-Length` and
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
// ...
Eof(bool),
}
Decoder::new 按 DecodedLength 选:CHUNKED 建状态机,CLOSE_DELIMITED 建 Eof(false),其余是 Length(n)。DecodedLength::ZERO 不会走到这里,Conn::poll_read_head 看到长度为 0 就直接进 Reading::KeepAlive,不建 Decoder。服务端解析请求时从不产生 CLOSE_DELIMITED,所以 Eof 只出现在客户端读响应的时候。
decode 的签名:
pub(crate) fn decode<R: MemRead>(
&mut self,
cx: &mut Context<'_>,
body: &mut R,
) -> Poll<Result<Frame<Bytes>, io::Error>> {
返回 Frame<Bytes>,数据帧或 trailers 帧,http_body 的帧见 Service 与 Body。body 结束用一个空的数据帧表示,配合 is_eof(Length(0)、Chunked 到了 End、Eof(true))判断。错误是 io::Error,Conn 再包成 hyper::Error。
body 只需要实现一个方法,MemRead::read_mem:
fn read_mem(&mut self, cx: &mut Context<'_>, len: usize) -> Poll<io::Result<Bytes>> {
if !self.read_buf.is_empty() {
let n = std::cmp::min(len, self.read_buf.len());
Poll::Ready(Ok(self.read_buf.split_to(n).freeze()))
} else {
let n = ready!(self.poll_read_from_io(cx))?;
Poll::Ready(Ok(self.read_buf.split_to(::std::cmp::min(len, n)).freeze()))
}
}
最多给 len 个字节;缓冲里有多少给多少,空了才去读一次 socket。给出去的是从读缓冲 split_to 切下来的 Bytes,和读缓冲共享内存,不拷贝。返回空 Bytes 表示对端关闭了连接。
Length
Length(remaining) => {
if *remaining == 0 {
Poll::Ready(Ok(Frame::data(Bytes::new())))
} else {
let to_read = usize::try_from(*remaining).unwrap_or(usize::MAX);
let buf = ready!(body.read_mem(cx, to_read))?;
let num = buf.as_ref().len() as u64;
if num > *remaining {
*remaining = 0;
} else if num == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
IncompleteBody,
)));
} else {
*remaining -= num;
}
Poll::Ready(Ok(Frame::data(buf)))
}
}
每次要「剩下的全部」,read_mem 给多少就交出多少。所以帧的边界跟着 socket 读的边界走,和对端写了几次无关。usize::try_from(...).unwrap_or(usize::MAX) 是 1.10.0 的 #4056 改的:之前直接 as usize,在 32 位系统上超过 4 GiB 的长度会被截断。对端没发够就关了连接,read_mem 返回空,报 IncompleteBody:「end of file before message length reached」。
Eof
Eof(is_eof) => {
if *is_eof {
Poll::Ready(Ok(Frame::data(Bytes::new())))
} else {
// 8192 chosen because its about 2 packets, there probably
// won't be that much available, so don't have MemReaders
// allocate buffers to big
body.read_mem(cx, 8192).map_ok(|slice| {
*is_eof = slice.is_empty();
Frame::data(slice)
})
}
}
不知道长度,就一直读,读到空为止。读到空本身就是正常结束,不是错误。
Chunked:逐字节的状态机
chunked 的格式是一串块,每块是「十六进制长度、可选的扩展、CRLF、数据、CRLF」,最后是长度为 0 的块,后面可以跟 trailers,再以一个空行结束:
5;name=val\r\n
hello\r\n
0\r\n
x-sum: 42\r\n
\r\n
ChunkedState 有 13 个状态,step 每次推进一步:
stateDiagram-v2 state "End" as Finished [*] --> Start Start --> Size : 十六进制位 Size --> Size : 十六进制位 Size --> SizeLws : 空格或制表符 Size --> Extension : 分号 Size --> SizeLf : CR SizeLws --> SizeLws : 空格或制表符 SizeLws --> Extension : 分号 SizeLws --> SizeLf : CR Extension --> Extension : 其他字节 Extension --> SizeLf : CR SizeLf --> Body : LF 且长度大于 0 SizeLf --> EndCr : LF 且长度为 0 Body --> Body : 块没读完 Body --> BodyCr : 块读完 BodyCr --> BodyLf : CR BodyLf --> Start : LF EndCr --> EndLf : CR EndCr --> Trailer : 其他字节 Trailer --> Trailer : 其他字节 Trailer --> TrailerLf : CR TrailerLf --> EndCr : LF EndLf --> Finished : LF Finished --> [*]
除了 Body,每个状态都只读一个字节,用 byte! 宏:
macro_rules! byte (
($rdr:ident, $cx:expr) => ({
let buf = ready!($rdr.read_mem($cx, 1))?;
if !buf.is_empty() {
buf[0]
} else {
return Poll::Ready(Err(io::Error::new(io::ErrorKind::UnexpectedEof,
"unexpected EOF during chunk size line")));
}
})
);
ready! 让状态机在任何一个字节处都能停下来返回 Pending,下次从同一个状态接着走,状态全在 Kind::Chunked 的字段里,不需要额外的缓冲。块头只有几个字节,逐字节读的开销不大;块数据在 read_body 里一次要「这块剩下的全部」,和 Length 一样整段切出:
// cap remaining bytes at the max capacity of usize
let to_read = usize::try_from(*rem).unwrap_or(usize::MAX);
let slice = ready!(rdr.read_mem(cx, to_read))?;
let count = slice.len();
if count == 0 {
*rem = 0;
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
IncompleteBody,
)));
}
*buf = Some(slice);
*rem -= count as u64;
decode 里的循环一拿到 buf 就返回一个数据帧。所以一帧最多是一个块;一个块在 socket 上分两次到达,就是两帧。
块长度在 read_start / read_size 里逐位累加,乘 16 和加法都用 checked_*,溢出报「invalid chunk size: overflow」;第一位不是十六进制数字报「Invalid chunk size line: missing size digit」。块数据后面必须紧跟 CRLF,否则报「Invalid chunk body CR」或「Invalid chunk body LF」。
扩展:读了就扔,但有上限
// We don't care about extensions really at all. Just ignore them.
// They "end" at the next CRLF.
//
// However, some implementations may not check for the CR, so to save
// them from themselves, we reject extensions containing plain LF as
// well.
match byte!(rdr, cx) {
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
b'\n' => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid chunk extension contains newline",
))),
_ => {
*extensions_cnt += 1;
if *extensions_cnt >= CHUNKED_EXTENSIONS_LIMIT {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"chunk extensions over limit",
)))
} else {
Poll::Ready(Ok(ChunkedState::Extension))
}
} // no supported extensions
}
扩展的内容不解析、不交给用户。CHUNKED_EXTENSIONS_LIMIT 是 16 KiB,注释说「This limit is currentlty applied for the entire body, not per chunk」:extensions_cnt 整个 body 累计,不按块清零。拒绝裸 LF 是防着前后两个实现对「扩展在哪结束」理解不一样。
#3495(首次发布于 v1.1.0)之前,Extension 状态遇到 CR 以外的字节就留在原地,读多少都行。提交说明:「Sending large amounts of bytes in the extensions will waste CPU reaing and skipping them. This change adds an internal limit to how many bytes will be read and ignored in a single body, before returning an error.」扩展是逐字节读的,每个字节都要走一遍 step,读完就扔,Service 看不到任何数据,也就没法自己限制。这个限制是内部常量,没有配置项。
trailers
长度为 0 的块之后,EndCr 状态读到的如果不是 CR,就说明后面有 trailers。read_end_cr 这时才分配 trailers_buf(容量 64,注释说「64 will fit a single Expires header without reallocating」),之后 read_trailer 等状态把每个字节放进去。两道限制:
- 字节数:
h1_max_header_size,没设时用TRAILER_LIMIT,16 KiB。超了报「chunk trailers bytes over limit」。 - 行数:
h1_max_headers,没设时用DEFAULT_MAX_HEADERS,100。超了报「chunk trailers count overflow」。
到 End 后,decode_trailers 用 httparse::parse_headers 把缓冲解析成 HeaderMap,作为一个 trailers 帧返回。trailers 帧之后不再有空数据帧,Conn::poll_read_body 看到 trailers 帧就直接进 Reading::KeepAlive。
实验:解码出来的帧
处理函数把收到的每一帧记下来,作为响应 body 返回;服务端、exchange 和 err_chain 与解析与编码页的实验相同,spawn_server 去掉 title_case_headers。客户端换成按段写的 exchange_parts:每段之间睡 50ms,shutdown 为真时写完半关闭。
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http = "1"
http-body-util = "=0.1.5"
bytes = "1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["rt", "net", "macros", "io-util", "time", "sync"] }
// Like `exchange`, but writes `parts` 50ms apart, optionally half-closing after.
pub async fn exchange_parts(
addr: std::net::SocketAddr,
name: &str,
parts: &[&[u8]],
shutdown: bool,
rx: &mut mpsc::UnboundedReceiver<String>,
) {
println!("== {name}");
let mut tcp = TcpStream::connect(addr).await.unwrap();
for (i, p) in parts.iter().enumerate() {
if i > 0 {
tokio::time::sleep(Duration::from_millis(50)).await;
}
tcp.write_all(p).await.unwrap();
}
if shutdown {
tcp.shutdown().await.unwrap();
}
// ... the rest is the same as `exchange`
}
// Claims 3 bytes, delivers 6.
struct Long(bool);
impl Body for Long {
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"abcdef")))))
}
fn size_hint(&self) -> SizeHint {
SizeHint::with_exact(3)
}
}
type B = BoxBody<Bytes, Infallible>;
async fn handle(req: Request<Incoming>) -> Result<Response<B>, Infallible> {
match req.uri().path() {
"/frames" => {
// Report every frame the decoder hands us.
let mut body = req.into_body();
let mut seen = Vec::new();
while let Some(f) = body.frame().await {
match f {
Ok(f) if f.is_data() => seen.push(format!("data({})", f.into_data().unwrap().len())),
Ok(f) => seen.push(format!("trailers{:?}", f.into_trailers().unwrap())),
Err(e) => {
seen.push(format!("err({})", common::err_chain(&e)));
break;
}
}
}
Ok(Response::new(Full::new(Bytes::from(seen.join(" "))).boxed()))
}
"/trailers" => {
let mut t = HeaderMap::new();
t.insert("x-checksum", "42".parse().unwrap());
t.insert("set-cookie", "a=b".parse().unwrap());
let frames = vec![Ok::<_, Infallible>(Frame::data(Bytes::from("abc"))), Ok(Frame::trailers(t))];
Ok(
Response::builder()
.header("trailer", "x-checksum, set-cookie")
.body(StreamBody::new(futures::stream::iter(frames)).boxed())
.unwrap(),
)
}
"/long" => Ok(Response::new(Long(false).boxed())),
_ => Ok(Response::new(Full::new(Bytes::new()).boxed())),
}
}
用例:
const CHUNKED: &[u8] = b"POST /frames HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n";
let ext = format!("1;{}\r\n", "e".repeat(16 * 1024));
let big_trailer = format!("1\r\na\r\n0\r\nx-t: {}\r\n\r\n", "t".repeat(16 * 1024));
let r = &mut rx;
common::exchange_parts(
addr,
"chunked: ext + 2 chunks + trailer, one write",
&[&[CHUNKED, b"5;name=val\r\nhello\r\n6\r\n world\r\n0\r\nx-sum: 42\r\n\r\n"].concat()],
false,
r,
)
.await;
common::exchange_parts(addr, "chunked: one chunk split across writes", &[CHUNKED, b"a\r\n0123", b"456789\r\n0\r\n\r\n"], false, r).await;
common::exchange_parts(
addr,
"length 7 split across writes",
&[b"POST /frames HTTP/1.1\r\ncontent-length: 7\r\n\r\nabc", b"defg"],
false,
r,
)
.await;
common::exchange_parts(addr, "length 10, peer half-closes after 3", &[b"POST /frames HTTP/1.1\r\ncontent-length: 10\r\n\r\nabc"], true, r).await;
common::exchange_parts(addr, "chunk size overflow", &[CHUNKED, b"10000000000000000\r\n"], false, r).await;
common::exchange_parts(addr, "chunk data without CRLF", &[CHUNKED, b"3\r\nabcX"], false, r).await;
common::exchange_parts(addr, "16KiB chunk extension", &[CHUNKED, ext.as_bytes()], false, r).await;
common::exchange_parts(addr, "16KiB trailer", &[CHUNKED, big_trailer.as_bytes()], false, r).await;
common::exchange_parts(addr, "response trailers, no te", &[b"GET /trailers HTTP/1.1\r\n\r\n"], false, r).await;
common::exchange_parts(addr, "response trailers, te: trailers", &[b"GET /trailers HTTP/1.1\r\nte: trailers\r\n\r\n"], false, r).await;
common::exchange_parts(addr, "body longer than size_hint", &[b"GET /long HTTP/1.1\r\n\r\n"], false, r).await;
M2 Max、rustc 1.99.0、--release,连跑 3 次,除了 date 的值输出相同。第一项留给本页的动脑筋,这里是解码相关的其余几项:
== chunked: one chunk split across writes
HTTP/1.1 200 OK\r\n
content-length: 15\r\n
date: Fri, 23 Oct 2026 13:11:28 GMT\r\n
\r\n
data(4) data(6)
[still open]
server: Ok
== length 7 split across writes
HTTP/1.1 200 OK\r\n
content-length: 15\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
data(3) data(4)
[still open]
server: Ok
== length 10, peer half-closes after 3
HTTP/1.1 200 OK\r\n
content-length: 92\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
data(3) err(error reading a body from connection: end of file before message length reached)
[EOF]
server: Ok
== chunk size overflow
HTTP/1.1 200 OK\r\n
content-length: 71\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
err(error reading a body from connection: invalid chunk size: overflow)
[EOF]
server: Ok
== chunk data without CRLF
HTTP/1.1 200 OK\r\n
content-length: 72\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
data(3) err(error reading a body from connection: Invalid chunk body CR)
[EOF]
server: Ok
== 16KiB chunk extension
HTTP/1.1 200 OK\r\n
content-length: 70\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
err(error reading a body from connection: chunk extensions over limit)
[EOF]
server: Ok
== 16KiB trailer
HTTP/1.1 200 OK\r\n
content-length: 82\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
data(1) err(error reading a body from connection: chunk trailers bytes over limit)
[EOF]
server: Ok
- 一个 10 字节的块分两次到达,处理函数看到两个数据帧(4 和 6);
Content-Length: 7的 body 分两次到达,也是两帧。帧的大小反映的是 socket 读到了多少,不要指望它和对端的写入或块一一对应。 - body 解码出错不影响响应:处理函数拿到错误后照常返回,hyper 把响应写完,然后关连接(
[EOF]),serve_connection返回Ok。出错时Conn::poll_read_body把读状态置为Reading::Closed,keep-alive 随之关闭,后面的字节已经没法确定从哪开始是下一个请求了。 - 错误信息的前半句是
hyper::Error的「error reading a body from connection」,冒号后面是Decoder产生的io::Error。
body 读到哪一步时自动回 100 Continue、body 没读完响应就写完了怎么办,属于读写状态机;解码出的帧怎么经过容量为 1 的通道交给 Incoming,见 Incoming 与 body 通道。
Encoder:三种写法
Encoder 也是一个 Kind 加一个 is_last:
pub(crate) struct Encoder {
kind: Kind,
is_last: bool,
}
// ...
enum Kind {
/// An Encoder for when Transfer-Encoding includes `chunked`.
Chunked(Option<Vec<HeaderName>>),
/// An Encoder for when Content-Length is set.
///
/// Enforces that the body is not longer than the Content-Length header.
Length(u64),
/// An Encoder for when neither Content-Length nor Chunked encoding is set.
///
/// This is mostly only used with HTTP/1.0 with a length. This kind requires
/// the connection to be closed when the body is finished.
#[cfg(feature = "server")]
CloseDelimited,
}
Chunked 里的 Option<Vec<HeaderName>> 是响应头 Trailer 里声明过的名字,下面讲 trailers 时用到。CloseDelimited 只在服务端存在:客户端的请求不允许靠关连接来分界。is_last 是 Server::encode 算出的「这条消息之后关连接」。
encode 不写任何字节,只把用户的 buf 包一层:
let kind = match &mut self.kind {
Kind::Chunked(_) => {
trace!("encoding chunked {}B", len);
let buf = ChunkSize::new(len)
.chain(msg)
.chain(b"\r\n" as &'static [u8]);
BufKind::Chunked(buf)
}
Kind::Length(remaining) => {
trace!("sized write, len = {}", len);
match usize::try_from(*remaining) {
// Holding more than is owed, so write only what is left.
Ok(limit) if limit < len => {
*remaining = 0;
BufKind::Limited(msg.take(limit))
}
// Ok(_) => Owed at least what we hold, write all of it.
// Err(_) => Owed more than `usize` can represent, write all of it.
Ok(_) | Err(_) => {
*remaining -= len as u64;
BufKind::Exact(msg)
}
}
}
#[cfg(feature = "server")]
Kind::CloseDelimited => {
trace!("close delimited write {}B", len);
BufKind::Exact(msg)
}
};
结果是一个 EncodedBuf,内部是五种 BufKind 之一,每一种都是 bytes::Buf 的组合:
BufKind | 内容 |
|---|---|
Exact(B) | 用户的 buf 原样 |
Limited(Take<B>) | 只取前 remaining 个字节 |
Chunked(Chain<Chain<ChunkSize, B>, StaticBuf>) | 块头 + 数据 + \r\n |
ChunkedEnd(StaticBuf) | 0\r\n\r\n |
Trailers(Chain<Chain<StaticBuf, Bytes>, StaticBuf>) | 0\r\n + trailers + \r\n |
Chain 不拷贝,只是把几段串起来,chunks_vectored 能把每一段交成一个 IoSlice。用户的 Bytes 从 poll_frame 出来,到 writev 系统调用,中间可以一次都不拷贝。写缓冲是不是真的用 writev,见读写缓冲的写策略。
ChunkSize 是一个栈上的小数组:
const USIZE_BYTES: usize = std::mem::size_of::<usize>();
// each byte will become 2 hex
const CHUNK_SIZE_MAX_BYTES: usize = USIZE_BYTES * 2;
#[derive(Clone, Copy)]
struct ChunkSize {
bytes: [u8; CHUNK_SIZE_MAX_BYTES + 2],
pos: u8,
len: u8,
}
64 位上是 18 个字节:最多 16 位十六进制,加 CRLF。ChunkSize::new 用 write!(&mut size, "{len:X}\r\n") 写进去,大写十六进制、不补零。每一帧数据对应一个块,块的大小就是这一帧的大小:实验里 abc 和 defg 两帧编码成 3\r\nabc\r\n4\r\ndefg\r\n。
结束:end 与 encode_and_end
body 结束时(poll_frame 返回 None),Conn::end_body 调 end:
pub(crate) fn end<B>(&self) -> Result<Option<EncodedBuf<B>>, NotEof> {
match self.kind {
Kind::Length(0) => Ok(None),
Kind::Chunked(_) => Ok(Some(EncodedBuf {
kind: BufKind::ChunkedEnd(b"0\r\n\r\n"),
})),
#[cfg(feature = "server")]
Kind::CloseDelimited => Ok(None),
Kind::Length(n) => Err(NotEof(n)),
}
}
chunked 补一个结束块;Length 必须已经写满,还差 n 个字节就是 NotEof。如果最后一帧数据到来时 body 已经 is_end_stream(),Dispatcher 改调 Conn::write_body_and_end,encode_and_end 把结束标记和最后一块拼在一起:chunked 时是 ChunkSize + 数据 + "\r\n0\r\n\r\n",少一个 buf。
body 和声明的长度对不上时:
- 比声明的长:
encode用Take截断,remaining归零,Encoder变成is_eof,Conn进Writing::KeepAlive;Dispatcher发现不能再写 body,把 body 丢掉。多出来的字节悄悄没了,连接照常复用。 - 比声明的短:
end返回NotEof,Conn::end_body把它包成「user body write aborted: early end, expected N more bytes」,serve_connection以这个错误结束。
实验里 /long 声称 3 个字节、实际给 6 个(上面的输出),/short 声称 5 个、实际给 3 个(解析与编码页的 wire.rs):
== body longer than size_hint
HTTP/1.1 200 OK\r\n
content-length: 3\r\n
date: Fri, 23 Oct 2026 13:11:30 GMT\r\n
\r\n
abc
[still open]
server: Ok
== short body
[EOF]
server: Err(user body write aborted: early end, expected 2 more bytes)
short body 这一项客户端一个字节都没收到,连响应头都没有。Short 的两次 poll_frame 都立即就绪,所以 Dispatcher::poll_write 在一次调用里依次写了头、写了 abc、调了 end_body,这些字节都还在写缓冲里,没到 flush 那一步;end_body 的错误经 ? 直接从 poll_write 返回,整个连接 future 以错误结束,写缓冲随之丢弃。如果 body 在两帧之间返回过 Pending,poll_write 先返回 Pending,poll_loop 接着 poll_flush,头和第一帧就先发出去了。把 Short 换成中间挂起一次的版本:
// Claims 5 bytes, delivers 3, and is Pending once before ending.
struct ShortPending(u8);
impl Body for ShortPending {
type Data = Bytes;
type Error = Infallible;
fn poll_frame(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
self.0 += 1;
match self.0 {
1 => Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"abc"))))),
2 => {
cx.waker().wake_by_ref();
Poll::Pending
}
_ => Poll::Ready(None),
}
}
fn size_hint(&self) -> SizeHint {
SizeHint::with_exact(5)
}
}
连跑 3 次,除了 date 的值输出相同:
== short body, pending once
HTTP/1.1 200 OK\r\n
content-length: 5\r\n
date: Fri, 23 Oct 2026 13:20:53 GMT\r\n
\r\n
abc
[EOF]
server: Err(user body write aborted: early end, expected 2 more bytes)
客户端看到一个 content-length: 5 却只有 3 个字节、然后连接断开的响应。两种情况客户端都拿不到完整响应,只是失败的样子不同。
trailers:要满足三个条件
encode_trailers 只在 Kind::Chunked(Some(allowed)) 时工作:响应必须是 chunked,而且响应头里有 Trailer,Server::encode 把它的值解析成 HeaderName 列表放进 Encoder。写出的只是在 Trailer 里声明过、且通过 is_valid_trailer_field 的字段,后者排除 Authorization、Cache-Control、Content-Encoding、Content-Length、Content-Range、Content-Type、Host、Max-Forwards、Set-Cookie、Trailer、Transfer-Encoding、TE 这 12 个。服务端还多一个条件,在 Conn::write_trailers 里:
pub(crate) fn write_trailers(&mut self, trailers: HeaderMap) {
if T::is_server() && !self.state.allow_trailer_fields {
debug!("trailers not allowed to be sent");
return;
}
allow_trailer_fields 来自请求头 TE 里有没有 trailers(headers::te_is_trailers,1.11.1 起大小写不敏感、可以和别的值写在一起)。实验里同一个 /trailers 响应:
== response trailers, no te
HTTP/1.1 200 OK\r\n
trailer: x-checksum, set-cookie\r\n
transfer-encoding: chunked\r\n
date: Fri, 23 Oct 2026 13:11:29 GMT\r\n
\r\n
3\r\n
abc\r\n
0\r\n
\r\n
[still open]
server: Ok
== response trailers, te: trailers
HTTP/1.1 200 OK\r\n
trailer: x-checksum, set-cookie\r\n
transfer-encoding: chunked\r\n
date: Fri, 23 Oct 2026 13:11:30 GMT\r\n
\r\n
3\r\n
abc\r\n
0\r\n
x-checksum: 42\r\n
\r\n
[still open]
server: Ok
请求没带 te: trailers 时 trailers 帧被整个丢掉,write_trailers 直接返回,写状态还是 Writing::Body,body 结束时 end_body 照常补 0\r\n\r\n。带了的时候 x-checksum 写出,set-cookie 虽然在 Trailer 里声明了,但不是合法的 trailer 字段,也被丢掉。
问: 我的 gRPC-Web 或者流式接口在 HTTP/1.1 上发 trailers,抓包看服务端根本没发,日志里也没有错误。是 hyper 的 bug 吗?
答: 多半是三个条件缺了一个:响应不是 chunked(body 的 size_hint 是精确的,hyper 选了 Content-Length,trailers 无处可放);响应头里没写 Trailer 声明字段名;请求头里没有 TE: trailers。三种情况 hyper 都只打一条 debug!,不报错,trailers 帧就这么丢了。客户端方向(hyper 发请求 trailers)没有 TE 这一条,只看前两条。
和 max_buf_size 的关系
max_buf_size 限的是读缓冲,「message head is too large」只在解析消息头时触发(见读写缓冲)。body 不受它限制:Decoder 每次都把读缓冲里的字节 split_to 切走交给 Incoming,读缓冲不会因为 body 长而变大。Service 不读 body 时,Incoming 的通道满了,Dispatcher 不再调 poll_read_body,socket 也就不再读,这是背压,不是上限。读写缓冲页的实验里 1 MiB 的 Content-Length body(超过默认 max_buf_size 约 408 KiB)照常读完。body 的总大小要限制,得在 Service 里做,比如 http-body-util 的 Limited(见 http-body-util)。chunked 的扩展和 trailers 是例外,它们有上面那两个 16 KiB 的内部上限。
实验的第一项把请求头和下面的 body 在一次 write_all 里发出:
5;name=val\r\n
hello\r\n
6\r\n
world\r\n
0\r\n
x-sum: 42\r\n
\r\n
/frames 处理函数打印的是什么?
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次,除了 date 的值输出相同:
== chunked: ext + 2 chunks + trailer, one write
HTTP/1.1 200 OK\r\n
content-length: 39\r\n
date: Fri, 23 Oct 2026 13:11:28 GMT\r\n
\r\n
data(5) data(6) trailers{"x-sum": "42"}
[still open]
server: Ok
- 两个数据帧:整个 body 已经在读缓冲里,
read_body每次要「这块剩下的全部」,一次就拿到整块,decode拿到buf立即返回,所以一块正好一帧。两块不会合并成一帧,哪怕它们在同一次读里到达。 ;name=val被Extension状态逐字节读掉,计入extensions_cnt,不出现在任何帧里。- 0 长度的块之后,
EndCr读到x不是 CR,分配trailers_buf,最终decode_trailers解析出x-sum: 42,作为 trailers 帧返回。之后没有空数据帧:Conn看到 trailers 帧就结束了读。 - 请求里没有
TE: trailers也照样收到了 trailers:TE: trailers只约束服务端发送响应 trailers,接收方向没有这个条件。
小结
Decoder三种:Length(n)每次要剩下的全部,Eof每次最多 8192 字节读到关闭(只用于客户端读响应),Chunked是 13 个状态的状态机。字节都来自MemRead::read_mem,是从读缓冲切下来的Bytes,不拷贝。- chunked 的块头逐字节读,状态全在
Kind::Chunked里,可以在任何字节处挂起;块数据整段切出,一帧最多一块,一块可能拆成多帧。 - 块长度溢出、缺 CRLF、提前 EOF 都是 body 错误:
Service收到错误,响应照写,之后关连接。扩展整个 body 累计不超过 16 KiB(1.1.0 起);trailers 字节数默认 16 KiB、可由max_header_size改,行数受max_headers限制。 Encoder三种:Length截断多余字节,Chunked用ChunkSize+ 数据 + CRLF 的Chain包装每一帧,CloseDelimited只在服务端。encode不拷贝用户数据。- body 比声明的长,多余部分被悄悄丢掉;比声明的短,
serve_connection以「early end」错误结束,写缓冲里还没 flush 的头和数据一起丢弃。 - 服务端发响应 trailers 要同时满足:chunked、响应头有
Trailer声明、请求头有TE: trailers,而且字段不在 12 个禁用名单里;不满足时静默丢弃。 max_buf_size不限制 body 大小,限制 body 要在Service里做。