body 大小限制
主线位置:作用点主线的第一个时刻。
call时要么短路成 413,要么给请求 body 套一层计数器;真正的限制发生在请求 body 被读的时候,由读 body 的一方承担后果。call的全文、它和 axumDefaultBodyLimit嵌套时谁先触发,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 不大于 limit | min(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 模块。
问: 既然真正的限制在读 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 字节。