Rust in Everything
基于 http-body-util 0.1.5

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类型擦除
BodyExtframe、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 就有了这些方法。

📝NOTE
冷知识:这些东西 0.14 时都在 hyper 里

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's Once variant」。CHANGELOG 的迁移说明只有一句:「Use the types from http-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, a tokio::sync::mpsc wrapped in StreamBody should work.」
  • #3020「update Body trait to use Frames」,BREAKING CHANGE 里写:「The free functions hyper::body::to_bytes and aggregate have been removed. Similar functionality is on http_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 改的就是这个数。

❗IMPORTANT
动脑筋:三个上传,一个 1024 字节的上限

处理函数把请求 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。

📝NOTE
没有蠢问题

问: 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」,lower 4096 不小于额度 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。

💬 关联讨论

发起讨论
加载中...