限了 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数的是解压后的字节,关掉它之前要确认替代品在解压里面。
💬 关联讨论
发起讨论加载中...