Rust in Everything
基于 tower-http 0.7.1

body 大小限制

主线位置:作用点主线的第一个时刻。call 时要么短路成 413,要么给请求 body 套一层计数器;真正的限制发生在请求 body 被读的时候,由读 body 的一方承担后果。call 的全文、它和 axum DefaultBodyLimit 嵌套时谁先触发,axum 卷 其余常用中间件 已经跑过,这里讲类型和边界情况。

flowchart TB
  req[请求进来] --> cl{"Content-Length 能解析成 usize"}
  cl -->|"是,且大于 limit"| r413["短路:413 length limit exceeded"]
  cl -->|"是,且不大于 limit"| tight["额度 = min(limit, Content-Length)"]
  cl -->|"没有或解析失败"| full["额度 = limit"]
  tight --> wrap["body 换成 Limited<ReqBody>"]
  full --> wrap
  wrap --> inner[内层读 body]
  inner -->|超额| err["LengthLimitError,装在 BoxError 里"]

两种 body 类型

RequestBodyLimit 的 Service 实现同时改了请求和响应的 body 类型:

impl<ReqBody, ResBody, S> Service<Request<ReqBody>> for RequestBodyLimit<S>
where
    ResBody: Body,
    S: Service<Request<Limited<ReqBody>>, Response = Response<ResBody>>,
{
    type Response = Response<ResponseBody<ResBody>>;

请求方向,内层收到的是 tower-http 自己的 body::Limited<ReqBody>,里面包着 http_body_util::Limited,错误类型固定成 BoxError(第 83 行)。包一层的理由见 body 模块:不把 http-body-util 的类型写进公开 API。代价是超额错误和内层 body 自己的错误混在同一个 BoxError 里,要用 e.is::<http_body_util::LengthLimitError>() 区分。

响应方向,limit::ResponseBody 是一个两变体的枚举:

    enum ResponseBodyInner<B> {
        PayloadTooLarge {
            #[pin]
            body: Full<Bytes>,
        },
        Body {
            #[pin]
            body: B
        }
    }

413 的正文 length limit exceeded 自己带着(PayloadTooLarge 变体里的 Full<Bytes>),不用向内层的 body 类型要一个空值。所以和 四件套 里那几个短路模块不同,RequestBodyLimit 不要求 ResBody: Default;代价是两个变体要有同一个 Data 类型,于是要求内层 body 的 Data = Bytes。反过来,它的 ResponseBody 在内层实现 Default 时也实现 Default(第 35 行),Cors、Timeout 可以套在它外面。

短路的 future 也很简单。ResponseFuture::poll 在 PayloadTooLarge 变体上调 create_error_response,现场造 413 和 Content-Type: text/plain; charset=utf-8;call 时只存一个不带数据的变体。

Content-Length 的三种情况

call 里对 Content-Length 的处理是 to_str().ok()?.parse::<usize>().ok(),失败一律当成「没有」:

请求额度结果
Content-Length 大于 limit—内层不调用,413
Content-Length 不大于 limitmin(limit, Content-Length)读超过 Content-Length 的部分就报错
没有,或者解析不了limit读超过 limit 就报错

第二行的「收紧」是为了不让 body 比自己声明的长。经 hyper 的 HTTP/1.1 进来的请求,body 本来就按 Content-Length 截断;这条对自己构造请求、或者 body 来源不可信的场合才有意义。第三行意味着 Content-Length: abc 不会被当成错误请求拒绝,而是当作长度未知。

模块文档还提醒了一点:body 超额以后,连接上剩下的字节怎么办不归这一层管,「If you're using hyper this is automatically handled for you」。

tower-http = { version = "=0.7.1", features = ["limit", "decompression-gzip"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
futures = "0.3"
flate2 = "1"
use bytes::Bytes;
use futures::stream;
use http::{header, Request, Response};
use http_body::Frame;
use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Full, LengthLimitError, StreamBody};
use std::{convert::Infallible, io::Write};
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::{decompression::RequestDecompressionLayer, limit::RequestBodyLimitLayer};

type Body = UnsyncBoxBody<Bytes, Infallible>;

// a streaming body of `len` bytes in 1 KiB frames, no size hint
fn stream_body(len: usize) -> Body {
  let frames: Vec<Result<Frame<Bytes>, Infallible>> =
    vec![b'a'; len].chunks(1024).map(|c| Ok(Frame::data(Bytes::copy_from_slice(c)))).collect();
  StreamBody::new(stream::iter(frames)).boxed_unsync()
}

// reads the whole body and reports what it saw
async fn handler<B>(req: Request<B>) -> Result<Response<Full<Bytes>>, Infallible>
where
  B: http_body::Body,
  B::Error: Into<BoxError>,
{
  let msg = match req.into_body().collect().await {
    Ok(c) => format!("read {} bytes", c.to_bytes().len()),
    Err(e) => {
      let e: BoxError = e.into();
      format!("error {:?}, LengthLimitError? {}", e.to_string(), e.is::<LengthLimitError>())
    }
  };
  Ok(Response::new(Full::from(msg)))
}

async fn send(name: &str, content_length: Option<&str>, body: Body) {
  let svc = ServiceBuilder::new().layer(RequestBodyLimitLayer::new(4096)).service_fn(handler);
  let mut req = Request::post("/");
  if let Some(cl) = content_length {
    req = req.header(header::CONTENT_LENGTH, cl);
  }
  let res = svc.oneshot(req.body(body).unwrap()).await.unwrap();
  let status = res.status();
  let ct = res.headers().get(header::CONTENT_TYPE).map(|v| v.to_str().unwrap().to_owned());
  let body = res.into_body().collect().await.unwrap().to_bytes();
  println!("{name:<32} {} {ct:?} {body:?}", status.as_u16());
}

fn gzip(data: &[u8]) -> Vec<u8> {
  let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
  e.write_all(data).unwrap();
  e.finish().unwrap()
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  println!("limit 4096");
  send("Content-Length: 5000", Some("5000"), stream_body(5000)).await;
  send("Content-Length: abc, 5000 bytes", Some("abc"), stream_body(5000)).await;
  send("Content-Length: 10, 100 bytes", Some("10"), stream_body(100)).await;
  send("no Content-Length, 3000 bytes", None, stream_body(3000)).await;
  send("no Content-Length, 5000 bytes", None, stream_body(5000)).await;

  let bomb = Bytes::from(gzip(&vec![0u8; 10 << 20]));
  println!("gzip of 10 MiB zeros: {} bytes, limit 1 MiB", bomb.len());
  let req = || {
    Request::post("/")
      .header(header::CONTENT_ENCODING, "gzip")
      .header(header::CONTENT_LENGTH, bomb.len())
      .body(Full::new(bomb.clone()))
      .unwrap()
  };
  let outside = ServiceBuilder::new()
    .layer(RequestBodyLimitLayer::new(1 << 20))
    .layer(RequestDecompressionLayer::new())
    .service_fn(handler);
  let res = outside.oneshot(req()).await.unwrap();
  println!("limit outside decompression: {}", String::from_utf8_lossy(&res.into_body().collect().await.unwrap().to_bytes()));
  let inside = ServiceBuilder::new()
    .layer(RequestDecompressionLayer::new())
    .layer(RequestBodyLimitLayer::new(1 << 20))
    .service_fn(handler);
  let res = inside.oneshot(req()).await.unwrap();
  println!("limit inside decompression:  {}", String::from_utf8_lossy(&res.into_body().collect().await.unwrap().to_bytes()));
}

输出(M2 Max、rustc 1.99.0,跑 3 次相同):

limit 4096
Content-Length: 5000             413 Some("text/plain; charset=utf-8") b"length limit exceeded"
Content-Length: abc, 5000 bytes  200 None b"error \"length limit exceeded\", LengthLimitError? true"
Content-Length: 10, 100 bytes    200 None b"error \"length limit exceeded\", LengthLimitError? true"
no Content-Length, 3000 bytes    200 None b"read 3000 bytes"
no Content-Length, 5000 bytes    200 None b"error \"length limit exceeded\", LengthLimitError? true"
gzip of 10 MiB zeros: 10208 bytes, limit 1 MiB
limit outside decompression: read 10485760 bytes
limit inside decompression:  error "length limit exceeded", LengthLimitError? true

前五行:只有第一行是这一层自己回的 413;其余超额的情况,状态码是 handler 决定的(这里 handler 把错误写进 200 的正文),RequestBodyLimit 只负责让读 body 的一方拿到 LengthLimitError。axum 的 Bytes、Json 等提取器会把这个错误换成 413,这一点见 axum 卷。

和请求解压的顺序

最后两行是同一个 gzip 炸弹:10 MiB 的零压成 10208 字节,限额 1 MiB。

flowchart TB
  subgraph outside["限额在外:限的是压缩后的字节"]
    a1["RequestBodyLimit:Content-Length 10208,额度 10208"] --> a2["RequestDecompression:解压,去掉 Content-Length"]
    a2 --> a3["handler 读到 10485760 字节"]
  end
  subgraph inside["限额在内:限的是解压后的字节"]
    b1["RequestDecompression:解压,去掉 Content-Length"] --> b2["RequestBodyLimit:没有 Content-Length,额度 1 MiB"]
    b2 --> b3["handler 读到 LengthLimitError"]
  end
  • 限额在外:RequestBodyLimit 看到的是压缩后的 body,Content-Length 只有 10208,远低于 1 MiB,额度收紧成 10208,压缩后的字节一个不多,全部通过;解压在它里面发生,解出来多少它都不知道。
  • 限额在内:RequestDecompression 解压时去掉了 Content-Length,RequestBodyLimit 按「长度未知」处理,额度是 1 MiB,数的是解压后的字节,读到第 1 MiB 多一点就报错。

RequestDecompression 本身没有解压后的大小上限,限额要靠把 RequestBodyLimit 放在它里面。它为什么去掉 Content-Length、不支持的编码怎么回 415,见 请求解压。两种顺序下响应的 body 类型也不一样(限额在外时是 limit::ResponseBody<UnsyncBoxBody<..>>,在内时是 UnsyncBoxBody<..>),按条件切换顺序时会遇到 if / else 类型不一致,原因见 body 模块。

📝NOTE
没有蠢问题

问: 既然真正的限制在读 body 时才发生,handler 不读 body 会怎样?

答: 什么都不会发生。Limited 只在 poll_frame 里计数,没人 poll 就没有错误,内层照常返回响应。模块文档原话是「no error will be generated if the body is never read」。剩下没读的字节由服务器处理,模块文档对 hyper 的说法是「automatically handled for you」。

小结

  • RequestBodyLimit 在 call 时按 Content-Length 决定:大于 limit 当场 413,否则额度取 min(limit, Content-Length),没有或解析失败用 limit。
  • 内层收到 body::Limited<ReqBody>,错误是 BoxError,用 is::<LengthLimitError>() 识别超额;超额时回什么状态码由读 body 的一方决定。
  • 响应 body 是自带 413 正文的两变体枚举,所以这一层不要求内层 body 实现 Default,但要求 Data = Bytes;它自己的 ResponseBody 在内层实现 Default 时也实现。
  • 和 RequestDecompression 一起用时,限额要放在解压里面,否则限的是压缩后的字节:实测 10208 字节的 gzip 在外层 1 MiB 限额下解出了 10485760 字节。

💬 关联讨论

发起讨论
加载中...