http-body-util
主线位置:两条主线的两端。服务端主线上,处理函数用它读请求 body(
collect、Limited),再用它造响应 body(Full、StreamBody、BoxBody),Dispatcher一帧一帧 poll 这个 body 写出去。客户端主线上,你发出的请求 body 也是这些类型。
flowchart TB
subgraph make ["造 body"]
full[Full]
empty[Empty]
sb[StreamBody]
ch["Channel(feature channel)"]
end
subgraph wrap ["包一层"]
lim[Limited]
me["map_err 等组合子"]
boxnode["BoxBody 或 UnsyncBoxBody"]
end
subgraph read ["读 body"]
fr["frame()"]
col["collect() 得到 Collected"]
end
inc[Incoming] --> lim
lim --> col
inc --> fr
make --> me
me --> boxnode
boxnode --> resp["Response 交给 hyper"]
hyper 只实现了一个 Body:收到的 Incoming。要发出去的 body 必须是别的类型,而 Body trait 本身(http-body crate,见 Service 与 Body)只有 poll_frame、is_end_stream、size_hint 三个方法,没有任何便利函数。http-body-util 补上这两块:几种现成的 body,和一个扩展 trait BodyExt。它和 hyper 同属 hyperium,但是独立的 crate,源码在 http-body 仓库里。
本页按 http-body-util 0.1.5 讲。本站 Cargo.lock 锁的是 0.1.3;按它的 CHANGELOG,0.1.4 加了 BodyExt::fuse / into_stream、Full::into_inner、InspectFrame / InspectErr,0.1.5 只修了文档渲染。下面用到的类型在 0.1.3 里都有。
有什么
| 类型或方法 | 作用 |
|---|---|
Full<D> | 一整块数据,一帧交完 |
Empty<D> | 没有数据 |
StreamBody<S> | 把一个 Stream<Item = Result<Frame<D>, E>> 当 body |
Channel<D, E> | tokio mpsc 做的 body,要开 channel feature(会依赖 tokio 的 sync) |
Either<L, R> | 两种 body 二选一 |
Limited<B> | 超过字节上限就报错 |
BoxBody / UnsyncBoxBody | 类型擦除 |
BodyExt | frame、collect、map_err、map_frame、boxed、boxed_unsync、with_trailers、into_data_stream 等 |
Collected<B> | collect 的结果:所有数据块和合并后的 trailers |
BodyExt 对所有 Body 都有一个空的 blanket 实现(impl<T: ?Sized> BodyExt for T where T: http_body::Body {}),use http_body_util::BodyExt 之后 Incoming 就有了这些方法。
hyper 0.14 的 hyper::Body 是一个结构体,能 Body::from("...")、Body::empty()、Body::channel(),读 body 用 hyper::body::to_bytes(body) 和 aggregate。1.0 的几个 PR 把它们一项一项拿掉了,都最早出现在 v1.0.0-rc.1:
- #2958「remove "full" constructors from
Body」,对应 issue #2922「Remove Body'sOncevariant」。CHANGELOG 的迁移说明只有一句:「Use the types fromhttp-body-util.」 - #2970「make body::Sender and Body::channel private」。issue #2962 给的替代办法是「This might be as simple as a
Full, but if specifically showing a channel usage, or needing a channel behavior for a test, atokio::sync::mpscwrapped inStreamBodyshould work.」 - #3020「update
Bodytrait to useFrames」,BREAKING CHANGE 里写:「The free functionshyper::body::to_bytesandaggregatehave been removed. Similar functionality is onhttp_body_util::BodyExt::collect.」
留在 hyper 里的那个结构体就是今天的 Incoming,见 Incoming 与 body 通道 的冷知识。
现成的 body
Full 与 Empty
Full 里是一个 Option<D>,poll_frame 把它 take 走,第一次交出一帧数据,之后都是 None:
impl<D> Full<D>
where
D: Buf,
{
/// Create a new `Full`.
pub fn new(data: D) -> Self {
let data = if data.has_remaining() {
Some(data)
} else {
None
};
Full { data }
}
}
impl<D> Body for Full<D>
where
D: Buf,
{
type Data = D;
type Error = Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<D>, Self::Error>>> {
Poll::Ready(self.data.take().map(|d| Ok(Frame::data(d))))
}
fn is_end_stream(&self) -> bool {
self.data.is_none()
}
fn size_hint(&self) -> SizeHint {
self.data
.as_ref()
.map(|data| SizeHint::with_exact(u64::try_from(data.remaining()).unwrap()))
.unwrap_or_else(|| SizeHint::with_exact(0))
}
}
三点和 hyper 直接相关:
size_hint是精确的,hyper 写Content-Length。Full::new收到空数据时直接存None,于是is_end_stream()一开始就是true,hyper 把它当作没有 body(见 Dispatcher 的poll_write)。Error = Infallible。和别的 body 放进同一个BoxBody时要先统一错误类型,下面「装箱」一节会用到。
Empty 更简单:零大小(只有一个 PhantomData),poll_frame 永远 Ready(None),is_end_stream 永远 true,size_hint 精确为 0。Empty::new 是 const fn。
StreamBody
StreamBody 把流的每一项原样当作一帧,流里可以有数据帧也可以有 trailers 帧。它没有覆盖 size_hint,用的是 trait 的默认值(下限 0、没有上限),所以 hyper 在 HTTP/1 上一定用 chunked。下面「装箱」一节的实验能看到这三种 body 在线上的样子。
Channel
Channel::new(buffer) 返回 (Sender, Channel),内部是一个容量为 buffer 的 tokio mpsc::Receiver<Frame<D>> 加一个 oneshot::Receiver<E> 传错误。适合「另一个任务边算边发」的响应 body。它和 hyper 内部的 body/chan.rs 没有关系:那个通道只给 Incoming 用,外面拿不到。
读 body:frame 与 collect
BodyExt::frame 返回一个 future,完成时就是下一帧(Option<Result<Frame<D>, E>>),要求 body 是 Unpin(Incoming 是)。一帧一帧读、处理完一帧再要下一帧,背压就一路传到 socket:hyper 的槽空了才读下一块。
BodyExt::collect 返回 Collect future,它在一个循环里不停 poll_frame,直到 None 或第一个错误:
impl<T: Body + ?Sized> Future for Collect<T> {
type Output = Result<crate::Collected<T::Data>, T::Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<Self::Output> {
let mut me = self.project();
loop {
let frame = ready!(me.body.as_mut().poll_frame(cx));
let frame = if let Some(frame) = frame {
frame?
} else {
return Poll::Ready(Ok(me.collected.take().expect("polled after complete")));
};
me.collected.as_mut().unwrap().push_frame(frame);
}
}
}
Collected 里是一个 VecDeque 做的 BufList,数据块原样排队,不拼接;空的数据块不入队;多个 trailers 帧合并成一个 HeaderMap。取结果有两种:
to_bytes():只有一块时对那一块调copy_to_bytes,块是Bytes时这一步不拷贝;多块时新建一个BytesMut把它们拷在一起。上一页 256 KiB 的 body 分成了 6 块,to_bytes就要拷一次 256 KiB。aggregate():返回impl Buf,不拷贝,适合交给按Buf读的解析器。
collect 会把整个 body 读进内存,而 body 多大由发送方说了算。hyper 自己的文档(body/mod.rs)在示例里写的是「Consider using Limited if the body is untrusted.」
Limited:给 body 设上限
Limited 只有两个字段,剩余额度和内层 body:
impl<B> Body for Limited<B>
where
B: Body,
B::Error: Into<Box<dyn Error + Send + Sync>>,
{
type Data = B::Data;
type Error = Box<dyn Error + Send + Sync>;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let this = self.project();
let res = match this.inner.poll_frame(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => None,
Poll::Ready(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
if data.remaining() > *this.remaining {
*this.remaining = 0;
Some(Err(LengthLimitError.into()))
} else {
*this.remaining -= data.remaining();
Some(Ok(frame))
}
} else {
Some(Ok(frame))
}
}
Poll::Ready(Some(Err(err))) => Some(Err(err.into())),
};
Poll::Ready(res)
}
- 按帧检查,事后检查。它不看
Content-Length,也不预读;每来一帧数据,比较这一帧的长度和剩余额度。越界的那一帧整个被拒,不会截断后交出前半截。所以「额度 1024、第一帧就有 4096 字节」时,一个字节也交不出来。 - 错误类型被擦除。
Error是Box<dyn Error + Send + Sync>,内层的错误(比如hyper::Error)和超限错误装在同一个类型里。要区分,用downcast_ref::<LengthLimitError>()。LengthLimitError是#[non_exhaustive]的单元结构体,Display是length limit exceeded。 - trailers 帧不计入额度,原样放行。
size_hint 也被改写了:
fn size_hint(&self) -> SizeHint {
use std::convert::TryFrom;
match u64::try_from(self.remaining) {
Ok(n) => {
let mut hint = self.inner.size_hint();
if hint.lower() >= n {
hint.set_exact(n)
} else if let Some(max) = hint.upper() {
hint.set_upper(n.min(max))
} else {
hint.set_upper(n)
}
hint
}
Err(_) => self.inner.size_hint(),
}
}
上限被压到剩余额度以内。注意第一个分支:内层至少还有 lower 字节、而额度更小时,它报告「正好 n 字节」。动脑筋里会看到这个数。
axum 的默认请求体上限就是它。axum-core 0.5.6(本站锁的版本)里,Bytes 等提取器先调 into_limited_body(),with_limited_body 在没有配置时用 Limited::new(b, DEFAULT_LIMIT),DEFAULT_LIMIT 是 2_097_152(2 MiB),DefaultBodyLimit 改的就是这个数。
处理函数把请求 body 包进 Limited::new(body, 1024),先打印 size_hint,再一帧一帧读;超限就回 413,正文是错误的 Display。客户端用裸 TCP 发三个请求,每个都用一条新连接:Content-Length: 1000、Content-Length: 4096、chunked 的 8 个 512 字节块(一次写完)。
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["full"] }
use std::{convert::Infallible, time::Duration};
use http_body_util::{BodyExt, Full, LengthLimitError, Limited};
use hyper::{
Request, Response, StatusCode,
body::{Body, Bytes, Incoming},
server::conn::http1,
service::service_fn,
};
use hyper_util::rt::TokioIo;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
time::timeout,
};
const LIMIT: usize = 1024;
async fn upload(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let body = Limited::new(req.into_body(), LIMIT);
let hint = body.size_hint();
println!(" server: Limited size_hint lower {} upper {:?}", hint.lower(), hint.upper());
let mut body = body;
let mut got = 0;
let res = loop {
match body.frame().await {
None => break Ok(()),
Some(Ok(frame)) => {
let n = frame.data_ref().map_or(0, |d| d.len());
got += n;
println!(" server: frame {n} bytes, accepted so far {got}");
}
Some(Err(e)) => break Err(e),
}
};
let rsp = match res {
Ok(()) => Response::new(Full::new(Bytes::from(format!("{got} bytes")))),
Err(e) => {
println!(
" server: error Display {:?}, Debug {e:?}, is LengthLimitError: {}",
e.to_string(),
e.downcast_ref::<LengthLimitError>().is_some()
);
let mut r = Response::new(Full::new(Bytes::from(e.to_string())));
*r.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
r
}
};
Ok(rsp)
}
async fn send(addr: std::net::SocketAddr, name: &str, req: Vec<u8>) {
println!("== {name}");
let mut tcp = TcpStream::connect(addr).await.unwrap();
tcp.write_all(&req).await.unwrap();
let mut got = Vec::new();
let mut buf = [0u8; 8192];
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]",
}
};
let text = String::from_utf8_lossy(&got);
let status = text.lines().next().unwrap_or("");
let body = text.split("\r\n\r\n").nth(1).unwrap_or("");
println!(" client: {status} | body {body:?} | then {end}");
drop(tcp);
tokio::time::sleep(Duration::from_millis(50)).await;
}
#[tokio::main]
async fn main() {
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();
tokio::spawn(async move {
let r = http1::Builder::new().serve_connection(TokioIo::new(stream), service_fn(upload)).await;
println!(" server: connection ended {r:?}");
});
}
});
let with_len = |n: usize| {
let mut v = format!("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: {n}\r\n\r\n").into_bytes();
v.extend(std::iter::repeat_n(b'a', n));
v
};
send(addr, "content-length 1000", with_len(1000)).await;
send(addr, "content-length 4096", with_len(4096)).await;
let mut chunked = b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n".to_vec();
for _ in 0..8 {
chunked.extend_from_slice(b"200\r\n");
chunked.extend(std::iter::repeat_n(b'a', 512));
chunked.extend_from_slice(b"\r\n");
}
chunked.extend_from_slice(b"0\r\n\r\n");
send(addr, "chunked, 8 chunks of 512", chunked).await;
}
Content-Length: 4096 那个请求,Limited 的 size_hint 报多少?chunked 那个,处理函数收下了几帧?三条连接在 413 之后各是开着还是关了?答案在本页末尾。
什么时候需要装箱
Response<B> 只能有一个 B。处理函数在不同分支里返回不同的 body 类型,编译不过:
use std::convert::Infallible;
use http_body_util::{Empty, Full};
use hyper::{body::{Body, Bytes, Incoming}, Request, Response};
async fn handler(req: Request<Incoming>) -> Result<Response<impl Body>, Infallible> {
if req.uri().path() == "/" {
Ok(Response::new(Full::new(Bytes::from_static(b"hi"))))
} else {
Ok(Response::new(Empty::<Bytes>::new()))
}
}
error[E0308]: mismatched types
--> src/bin/box_err.rs:9:22
|
9 | Ok(Response::new(Empty::<Bytes>::new()))
| ------------- ^^^^^^^^^^^^^^^^^^^^^ expected `Full<Bytes>`, found `Empty<Bytes>`
| |
| arguments to this function are incorrect
|
= note: expected struct `http_body_util::Full<hyper::body::Bytes>`
found struct `http_body_util::Empty<hyper::body::Bytes>`
impl Body 也救不了:返回位置的 impl Trait 仍然是一个具体类型。三种办法:
| 办法 | 代价 | 适合 |
|---|---|---|
两个分支都用同一个类型,比如空响应写成 Full::new(Bytes::new()) | 没有 | 分支都是「一整块」 |
Either<L, R> | 只能两种(可以嵌套);Error 变成 Box<dyn Error + Send + Sync> | 两种 body,不想分配 |
.boxed() / .boxed_unsync() | 每个响应一次堆分配,每帧一次虚调用;所有分支的 Data、Error 必须先统一 | 分支多、类型杂(路由框架) |
BoxBody 就是一个 Pin<Box<dyn Body<Data = D, Error = E> + Send + Sync + 'static>>,三个方法都直接转发:
impl<D, E> Body for BoxBody<D, E>
where
D: Buf,
{
type Data = D;
type Error = E;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
self.inner.as_mut().poll_frame(cx)
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> SizeHint {
self.inner.size_hint()
}
}
size_hint 和 is_end_stream 都转发,所以装箱不改变 hyper 选 Content-Length 还是 chunked。三种 body 装进同一个 BoxBody<Bytes, Infallible>,看线上的字节(关掉 Date 头让输出稳定):
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["full"] }
futures = "0.3"
use std::{convert::Infallible, time::Duration};
use futures::stream;
use http_body_util::{BodyExt, Empty, Full, StreamBody, combinators::BoxBody};
use hyper::{
Request, Response,
body::{Body, Bytes, Frame, Incoming},
server::conn::http1,
service::service_fn,
};
use hyper_util::rt::TokioIo;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
time::timeout,
};
async fn handler(req: Request<Incoming>) -> Result<Response<BoxBody<Bytes, Infallible>>, Infallible> {
let body = match req.uri().path() {
"/full" => Full::new(Bytes::from_static(b"hello")).boxed(),
"/empty" => Empty::<Bytes>::new().boxed(),
_ => {
let chunks = ["hel", "lo"].map(|s| Ok::<_, Infallible>(Frame::data(Bytes::from_static(s.as_bytes()))));
StreamBody::new(stream::iter(chunks)).boxed()
}
};
let hint = body.size_hint();
println!("{:<8} size_hint exact {:?}, is_end_stream {}", req.uri().path(), hint.exact(), body.is_end_stream());
Ok(Response::new(body))
}
#[tokio::main]
async fn main() {
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();
http1::Builder::new()
.auto_date_header(false)
.serve_connection(TokioIo::new(stream), service_fn(handler))
.await
.unwrap();
});
let mut tcp = TcpStream::connect(addr).await.unwrap();
for path in ["/full", "/empty", "/stream"] {
tcp.write_all(format!("GET {path} HTTP/1.1\r\nHost: x\r\n\r\n").as_bytes()).await.unwrap();
let mut got = Vec::new();
let mut buf = [0u8; 1024];
while let Ok(Ok(n)) = timeout(Duration::from_millis(100), tcp.read(&mut buf)).await {
got.extend_from_slice(&buf[..n]);
}
println!(" wire: {:?}", String::from_utf8_lossy(&got));
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
/full size_hint exact Some(5), is_end_stream false
wire: "HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello"
/empty size_hint exact Some(0), is_end_stream true
wire: "HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n"
/stream size_hint exact None, is_end_stream false
wire: "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n3\r\nhel\r\n2\r\nlo\r\n0\r\n\r\n"
装箱之后 Full 照样是 content-length: 5,StreamBody 照样是 chunked,每个流元素一个 chunk。Empty 的 is_end_stream 为真,hyper 不写 body,但仍然写了 content-length: 0。头怎么编码见 解析与编码。
三个分支能放进同一个 BoxBody<Bytes, Infallible>,是因为它们的 Error 碰巧都是 Infallible。错误类型不同时先 .map_err(...) 统一;把 Incoming(Error = hyper::Error)和 Full(Error = Infallible)放在一起,常见写法是 full.map_err(|never| match never {}).boxed(),统一成 hyper::Error。
问: BoxBody 和 UnsyncBoxBody 只差一个 Sync,该用哪个?
答: hyper 自己从不要求 body 是 Sync:serve_connection 对响应 body 只要求 'static,以及错误能转成 Box<dyn Error + Send + Sync>;Send 是 tokio::spawn 要的。BoxBody 要求被装的 body 是 Sync,而很多流不是,比如 Pin<Box<dyn Stream<Item = ...> + Send>>:
error[E0277]: `dyn Stream<Item = Result<Frame<Bytes>, Infallible>> + Send` cannot be shared between threads safely
--> src/bin/unsync.rs:12:33
|
12 | let _err = StreamBody::new(s).boxed();
| ^^^^^ `dyn Stream<Item = Result<Frame<Bytes>, Infallible>> + Send` cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `dyn Stream<Item = Result<Frame<Bytes>, Infallible>> + Send`
同一个流用 .boxed_unsync() 就能编译,返回 Response<UnsyncBoxBody<Bytes, Infallible>> 的服务在 http1 和 http2 的 serve_connection 下都能 tokio::spawn。axum 的 Body 内部就是 UnsyncBoxBody<Bytes, axum_core::Error>(axum-core 0.5.6 的 body.rs)。只有在你自己的代码需要跨线程共享 &body 时才需要 BoxBody。
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 4 次输出完全相同:
== content-length 1000
server: Limited size_hint lower 1000 upper Some(1000)
server: frame 1000 bytes, accepted so far 1000
client: HTTP/1.1 200 OK | body "1000 bytes" | then [still open]
server: connection ended Ok(())
== content-length 4096
server: Limited size_hint lower 1024 upper Some(1024)
server: error Display "length limit exceeded", Debug LengthLimitError, is LengthLimitError: true
client: HTTP/1.1 413 Payload Too Large | body "length limit exceeded" | then [still open]
server: connection ended Ok(())
== chunked, 8 chunks of 512
server: Limited size_hint lower 0 upper Some(1024)
server: frame 512 bytes, accepted so far 512
server: frame 512 bytes, accepted so far 1024
server: error Display "length limit exceeded", Debug LengthLimitError, is LengthLimitError: true
server: connection ended Ok(())
client: HTTP/1.1 413 Payload Too Large | body "length limit exceeded" | then [EOF]
- 1000 字节:
Incoming的size_hint是精确的 1000,小于额度,Limited用set_upper(min(1024, 1000)),仍是精确的 1000。一帧收完,200。 - 4096 字节:
Incoming说「正好 4096」,lower4096 不小于额度 1024,走set_exact(1024):Limited报告「正好 1024 字节」,而 body 实际有 4096。所以不能用包过之后的size_hint提前判断超限;要提前拒绝,得在包之前看Incoming的size_hint(也就是Content-Length)。4096 字节和请求头在一次读里一起到了,hyper 把它作为一帧交出,这一帧超过额度,整帧被拒,accepted一次也没打印。错误的原文是length limit exceeded,Debug是LengthLimitError,downcast_ref::<LengthLimitError>()认得出来。 - chunked:
Incoming不知道长度(下限 0、无上限),Limited把上限设为 1024。chunked 解码器每个 chunk 交一帧,前两帧 512 + 512 正好等于额度,收下;第三帧超过剩余的 0,报错。 - 连接:4096 那条在 413 之后仍然开着(
[still open],客户端关掉时服务端才ended):整个 body 早在处理函数报错前就已经读完、进了槽,drop 掉Incoming不影响连接。chunked 那条被服务端关了([EOF]):报错时 body 还剩 5 个 chunk,poll_drain_or_close_read只解码一步、只吃掉一个 chunk,读不完,连接在 413 之后关闭(见 Incoming 与 body 通道)。字节全在读缓冲里也一样,chunked 的「一步」是一个 chunk,不是一次读。
小结
- hyper 只实现了
Incoming;造 body、读 body、限 body 都靠 http-body-util。1.0 把Body::from、Body::channel、to_bytes从 hyper 里拿掉,迁移说明都指向这个 crate。 Full一帧交完,size_hint精确(Content-Length),空数据时is_end_stream一开始就为真;Empty永远为空;StreamBody不报长度,HTTP/1 上走 chunked。frame()逐帧读,保留背压;collect()读到结束,to_bytes()在多于一块时会拷贝,aggregate()不拷贝。Limited按帧检查,越界的那一帧整个被拒;错误是Box<dyn Error + Send + Sync>,原文length limit exceeded,用downcast_ref::<LengthLimitError>()区分。包过之后的size_hint可能把「正好 4096」报成「正好 1024」。axum 提取器默认的 2 MiB 上限就是它。- 不同分支返回不同 body 时:同一类型、
Either、或装箱。BoxBody转发size_hint和is_end_stream,不改变分帧方式;hyper 不要求Sync,流不是Sync时用UnsyncBoxBody。