Rust in Everything
基于 tower-http 0.7.1

限了 1 MiB,handler 还是读到了 10 MiB

主线位置:作用点主线的第一个时刻决定额度,第三个时刻才数字节。RequestBodyLimit 在 call 时按 Content-Length 定额度,RequestDecompression 在 call 时换 body、删掉 Content-Length,谁在外层,谁就先看到请求。两层的机制和纯 tower 下的实验在 body 大小限制 和 请求解压;这一页放进 axum,和 axum 自己的 DefaultBodyLimit 一起看。

flowchart TB
  req["POST,Content-Encoding: gzip,Content-Length: 10208"] --> outer{"最外层是谁"}
  outer -->|"RequestBodyLimit"| l1["额度 = min(1 MiB, 10208),数的是压缩字节"]
  l1 --> d1["解压:删掉 Content-Length"]
  d1 --> h1["Bytes 读到 10485760 字节"]
  outer -->|"RequestDecompression"| d2["解压:删掉 Content-Length"]
  d2 --> l2["额度 = 1 MiB,数的是解压后的字节"]
  l2 --> h2["Bytes 读到上限,413"]

复现

axum-core 的 DefaultBodyLimit::disable 文档给的搭配是先 .layer(DefaultBodyLimit::disable()),再 .layer(RequestBodyLimitLayer::new(..))。在这个基础上加请求解压,.layer 写在哪一行就决定了顺序:axum 里后写的 .layer 在外层。

tower-http = { version = "=0.7.1", features = ["limit", "decompression-gzip"] }
axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
flate2 = "1"
use axum::body::{Body, Bytes};
use axum::extract::DefaultBodyLimit;
use axum::http::{header, Request};
use axum::routing::post;
use axum::Router;
use std::io::Write;
use tower::ServiceExt;
use tower_http::{decompression::RequestDecompressionLayer, limit::RequestBodyLimitLayer};

const MIB: usize = 1 << 20;

// 10 MiB 的零,压成 gzip
fn bomb() -> Vec<u8> {
  let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
  e.write_all(&vec![0u8; 10 * MIB]).unwrap();
  e.finish().unwrap()
}

fn routes() -> Router {
  Router::new().route("/", post(|body: Bytes| async move { format!("handler got {} bytes", body.len()) }))
}

async fn send(name: &str, app: Router, gz: &[u8]) {
  let req = Request::post("/")
    .header(header::CONTENT_ENCODING, "gzip")
    .header(header::CONTENT_LENGTH, gz.len())
    .body(Body::from(gz.to_vec()))
    .unwrap();
  let res = app.oneshot(req).await.unwrap();
  let status = res.status();
  let body = axum::body::to_bytes(res.into_body(), usize::MAX).await.unwrap();
  println!("{name:<44} {status} {}", String::from_utf8_lossy(&body));
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let gz = bomb();
  println!("gzip body: {} bytes on the wire, {} bytes inflated", gz.len(), 10 * MIB);
  // axum:最后一个 .layer 在最外层
  send(
    "disable, decompression, limit (limit outer)",
    routes().layer(DefaultBodyLimit::disable()).layer(RequestDecompressionLayer::new()).layer(RequestBodyLimitLayer::new(MIB)),
    &gz,
  )
  .await;
  send(
    "disable, limit, decompression (limit inner)",
    routes().layer(DefaultBodyLimit::disable()).layer(RequestBodyLimitLayer::new(MIB)).layer(RequestDecompressionLayer::new()),
    &gz,
  )
  .await;
  send(
    "default 2 MiB, decompression, limit",
    routes().layer(RequestDecompressionLayer::new()).layer(RequestBodyLimitLayer::new(MIB)),
    &gz,
  )
  .await;
}

M2 Max、rustc 1.99.0、--release,flate2 1.1.10,跑 3 次输出相同:

gzip body: 10208 bytes on the wire, 10485760 bytes inflated
disable, decompression, limit (limit outer)  200 OK handler got 10485760 bytes
disable, limit, decompression (limit inner)  413 Payload Too Large Failed to buffer the request body: length limit exceeded
default 2 MiB, decompression, limit          413 Payload Too Large Failed to buffer the request body: length limit exceeded
  • 第一行就是这个坑:10208 字节过网,Bytes 读进了 10485760 字节,压缩比约一千比一。RequestBodyLimit 在最外层,看到 Content-Length: 10208,额度收紧到 10208(limit/service.rs 第 48 行 的 call 读这个头);这个额度数的是压缩字节,解压在它里面,展开多少它不知道。
  • 第二行:解压在外层,先在 decompression/request/service.rs 第 66 行 删掉 Content-Length;RequestBodyLimit 按长度未知处理,给解压后的流套 1 MiB 的 Limited。Bytes 读到上限拿到 LengthLimitError,axum 把它变成 413。
  • 第三行:没关 DefaultBodyLimit,顺序同第一行,仍然 413。axum 的默认限额是 Bytes 提取器在读 body 时套上去的,那时 body 已经是解压后的,所以默认的 2 MiB 数的正是解压后的字节。照文档「关掉默认值、换成 tower-http 的」反而拆掉了这道防线。

修法

  • RequestBodyLimitLayer 放在 RequestDecompressionLayer 里面。在 axum 里就是先写 .layer(RequestBodyLimitLayer::..),后写 .layer(RequestDecompressionLayer::..);在 ServiceBuilder 里相反,先写的在外面。
  • 想同时在网络层按 Content-Length 拒绝巨大的请求,外面再放一层限额,两层限的是两件事(请求解压 末尾的问答)。
  • 不要只为了「换成 tower-http 的限额」而 DefaultBodyLimit::disable();保留它,它在提取器那里数解压后的字节。

RequestDecompression 自己没有解压后上限,这是它的设计(设计取舍),所以这件事只能靠层的顺序。

小结

  • 限额在解压外面时,限的是压缩后的字节,10208 字节的 gzip 能让 handler 读到 10 MiB。
  • axum 里后写的 .layer 在外层,和 ServiceBuilder 相反,写反最常见。
  • axum 默认的 DefaultBodyLimit 数的是解压后的字节,关掉它之前要确认替代品在解压里面。

💬 关联讨论

发起讨论
加载中...