Failed to buffer the request body: length limit exceeded
主线位置:两条主线各占一半。上限是装配主线上
.layer(DefaultBodyLimit::max(..))定的,它只往请求的 extensions 里写一个值;真正的限制发生在请求主线上提取器读 body 的那一刻,超了就是 413,handler 不会被调用。
flowchart TB req["Request"] --> dbl["限额层:写扩展"] dbl --> ext["Bytes、Json 提取器"] ext --> wl["套上 Limited"] wl --> col["collect 读 body"] col -->|超额| lle["LengthLimitError"] lle --> fb["from_err 归类"] fb --> r413["413 Payload Too Large"] col --> handler["调用 handler"]
机制在 body 提取器 里讲过:默认上限 2_097_152 字节,DefaultBodyLimit 只是一个扩展,内层覆盖外层,直接拿 Request 的提取器不受限;它和 tower-http RequestBodyLimitLayer 叠在一起时谁先回答,在 其余常用中间件 里。这一页补上两件那两页没讲的事:上传超限时客户端在线上看到什么,以及为什么「明明加了 DefaultBodyLimit::max」还是 413。
复现:线上的 413
起一个真正的 axum::serve,handler 用 Bytes 收 body。先用裸 TcpStream 把整个请求一口气写完再读响应,再用 curl 8.7.1 上传同样大小的文件:
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "net", "io-util", "process"] }
use std::net::SocketAddr;
use axum::{Router, body::Bytes, routing::post};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
const MIB: usize = 1024 * 1024;
async fn len(body: Bytes) -> String {
format!("got {} bytes", body.len())
}
// write the whole request first, then read whatever comes back
async fn raw_post(addr: SocketAddr, path: &str, size: usize) {
let mut s = TcpStream::connect(addr).await.unwrap();
let head = format!("POST {path} HTTP/1.1\r\nhost: x\r\nconnection: close\r\ncontent-length: {size}\r\n\r\n");
let mut req = head.into_bytes();
req.extend(std::iter::repeat_n(b'a', size));
let write = s.write_all(&req).await.map_err(|e| e.kind());
let mut resp = Vec::new();
let mut buf = [0u8; 4096];
let end = loop {
match s.read(&mut buf).await {
Ok(0) => break "EOF".to_string(),
Ok(n) => resp.extend_from_slice(&buf[..n]),
Err(e) => break format!("Err({:?})", e.kind()),
}
};
let text = String::from_utf8_lossy(&resp);
let status = text.lines().next().unwrap_or("-");
let body = text.split("\r\n\r\n").nth(1).unwrap_or("-");
println!("{path:<8} {:>2} MiB: write {write:?}; read {status:?} {body:?}, then {end}", size / MIB);
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/upload", post(len));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
for size in [1 * MIB, 3 * MIB, 20 * MIB] {
raw_post(addr, "/upload", size).await;
}
// the same two uploads with curl; the file paths come from the command line
let url = format!("http://{addr}/upload");
for file in std::env::args().skip(1) {
let out = tokio::process::Command::new("curl")
.args(["-sS", "--noproxy", "*", "-w", " -> %{http_code}", "--data-binary"])
.arg(format!("@{file}"))
.arg(&url)
.output()
.await
.unwrap();
let name = file.rsplit('/').next().unwrap();
println!(
"curl {name:<10} exit {:?}: {} {}",
out.status.code(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr).trim_end()
);
}
}
两个参数是用 head -c 从 /dev/zero 生成的 3 MiB 和 20 MiB 文件。M2 Max、rustc 1.99.0、--release,其中一次的输出:
/upload 1 MiB: write Ok(()); read "HTTP/1.1 200 OK" "got 1048576 bytes", then EOF
/upload 3 MiB: write Ok(()); read "HTTP/1.1 413 Payload Too Large" "Failed to buffer the request body: length limit exceeded", then EOF
/upload 20 MiB: write Err(ConnectionReset); read "HTTP/1.1 413 Payload Too Large" "Failed to buffer the request body: length limit exceeded", then Err(ConnectionReset)
curl 3mib.bin exit Some(0): Failed to buffer the request body: length limit exceeded -> 413
curl 20mib.bin exit Some(0): Failed to buffer the request body: length limit exceeded -> 413
连跑 10 次,状态行、正文、curl 的两行每次相同,变的是连接怎么结束:
| 请求 | 10 次里的结果 |
|---|---|
| 3 MiB,裸 TCP | 写都成功;读完 413 之后 6 次 EOF、4 次 ConnectionReset |
| 20 MiB,裸 TCP | 写全部失败:8 次 BrokenPipe、2 次 ConnectionReset;之后仍读到了 413,再 ConnectionReset |
| curl 3 MiB、20 MiB | 10 次都是 413,退出码 0 |
Content-Length写得清清楚楚,服务端照样先读。Bytes的上限靠 http-body-util 的Limited,它在读的过程中计数,读到第 2 MiB 之后的那一帧才发现超额,不看Content-Length。3 MiB 的请求里,服务端读了 2 MiB 多才回 413。- 回完 413 连接就关了。请求 body 还有没读的部分,连接没法继续用;接收缓冲里还有数据时关闭 socket,内核发的是 RST,所以客户端读完 413 有时看到
ConnectionReset(原理和 message head is too large 里 431 之后的 reset 相同)。 - 20 MiB 时写会失败。客户端还有十几 MiB 没写出去,服务端已经关了,
write_all拿到BrokenPipe或ConnectionReset。413 的字节在那之前已经进了客户端的接收缓冲,所以这个程序接着读还能读到;但如果客户端在写失败时就放弃,看到的只是一个连接错误,不是 413。 - curl 每次都拿到 413。它一边发一边读,看到响应就停止上传。
所以「前端说上传失败、连接被重置,服务端日志却什么也没有」(拒绝只在 TRACE 级别记日志,见 json-rejections),先查是不是超了 2 MiB。
源码:这句话是两段拼起来的
Bytes::from_request 把 collect 的错误交给 FailedToBufferBody::from_err:
pub(crate) fn from_err<E>(err: E) -> Self
where
E: Into<BoxError>,
{
// two layers of boxes here because `with_limited_body`
// wraps the `http_body_util::Limited` in a `axum_core::Body`
// which also wraps the error type
let box_error = match err.into().downcast::<Error>() {
Ok(err) => err.into_inner(),
Err(err) => err,
};
let box_error = match box_error.downcast::<Error>() {
Ok(err) => err.into_inner(),
Err(err) => err,
};
match box_error.downcast::<http_body_util::LengthLimitError>() {
Ok(err) => Self::LengthLimitError(LengthLimitError::from_err(err)),
Err(err) => Self::UnknownBodyError(UnknownBodyError::from_err(err)),
}
}
拆掉两层 axum_core::Error,是 http-body-util 的 LengthLimitError 就归为 axum-core 自己的 LengthLimitError(#[status = PAYLOAD_TOO_LARGE]、#[body = "Failed to buffer the request body"]),否则是 UnknownBodyError(400,同样的前半句)。define_rejection! 生成的 Display 是「前半句 + : + 内部错误」,内部错误是 http-body-util 0.1.5 的 LengthLimitError,它的 Display 就是 length limit exceeded。
两个同名的 LengthLimitError 正好说明了分工:http-body-util 的那个在读 body 时产生,axum-core 的那个是 rejection。Bytes、String、Json、Form 走的都是这条路,所以它们的 413 正文完全相同;Multipart 读字段时超限,正文是 Request payload is too large,状态码同样 413(见 body 提取器 的 Multipart 一节)。
Content-Length 的提取器,0.6 删掉了axum 0.5 有 ContentLengthLimit<T, N>,包在别的提取器外面用,比如 ContentLengthLimit<String, 1024>。它的文档写着「This requires the request to have a Content-Length header」,没有头的请求另外推荐 tower_http::limit::RequestBodyLimitLayer。
0.6 开发期间先有了 DefaultBodyLimit::max(#1397,说明里列了当时设上限的三种办法,并提议:「I feel we should consider removing ContentLengthLimit. It can essentially be replaced with」post(handler.layer(DefaultBodyLimit::max(new_limit)))),随后 #1400「Remove ContentLengthLimit」把它删了,CHANGELOG 记在 0.6.0 下:「ContentLengthLimit has been removed. Use DefaultBodyLimit instead」。提交 896ffc5f 第一次出现在 axum-v0.6.0-rc.3。
#1400 关闭的 issue #1399 标题是「Remove ContentLengthLimit and make DefaultBodyLimit check Content-Length」,后一半没有实现:0.8.9 的 with_limited_body 只套 Limited,整个 DefaultBodyLimit 不读 Content-Length。这就是上面 3 MiB 的请求要先被读进 2 MiB 才被拒绝的原因。按 Content-Length 提前拒绝的是 tower-http 的 RequestBodyLimitLayer,它在头里看到超限就直接回 413,一个字节的 body 都不读。
修法
只放宽需要的路由。 上传接口单独设上限,其余接口保持 2 MiB:
let app = Router::new()
.route("/upload", post(upload).layer(DefaultBodyLimit::max(50 * 1024 * 1024)))
.route("/users", post(create_user));
DefaultBodyLimit 实现了 tower Layer,可以加在 MethodRouter、Router 上,也可以加在 handler 上(upload.layer(...));同一个请求经过几层,最靠近 handler 的那层说了算。
加在 Router 上要注意顺序。 Router::layer 只套在调用时已经存在的路由上,之后 .route 加进来的不受影响。下面的动脑筋就是这个;layer 的这条规则本身见 中间件没生效。
disable() 之后一定要有别的上限。 DefaultBodyLimit::disable() 关掉的是唯一一道防线,Bytes 会把任意大的 body 读进内存。axum-core 文档给的搭配是 .layer(DefaultBodyLimit::disable()) 再 .layer(RequestBodyLimitLayer::new(..)),两者的关系见 其余常用中间件。
想在读 body 之前就拒绝,加 RequestBodyLimitLayer:带 Content-Length 的超限请求在头部就被拒绝,客户端也就不会在写到一半时被 reset。
大文件不要整个读进内存。 超过几十 MiB 的上传,用 Multipart 逐个字段读,或者直接拿 Body 流式写盘;直接拿 Body 时 DefaultBodyLimit 不生效,要自己数字节或依赖 RequestBodyLimitLayer。
axum = "=0.8.9"
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread"] }
tower = { version = "=0.5.3", features = ["util"] }
use axum::{
Router,
body::{Body, Bytes},
extract::{DefaultBodyLimit, Request},
routing::post,
};
use tower::ServiceExt;
const MIB: usize = 1024 * 1024;
async fn len(body: Bytes) -> String {
format!("got {} bytes", body.len())
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/a", post(len).layer(DefaultBodyLimit::max(8 * MIB)))
.route("/b", post(len))
.layer(DefaultBodyLimit::max(8 * MIB))
.route("/c", post(len));
for path in ["/a", "/b", "/c"] {
let req = Request::post(path).body(Body::from(vec![b'a'; 3 * MIB])).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
println!("{path}: {}", res.status());
}
}
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
/a: 200 OK
/b: 200 OK
/c: 413 Payload Too Large
/a:路由自己的max(8 MiB),外面又被Router::layer的max(8 MiB)套了一层,两层值相同,200。/b:自己没设,但.layer(DefaultBodyLimit::max(8 * MIB))调用时它已经在路由表里了,被套上,200。/c:在.layer(...)之后才加进来,没有被套,用的是默认的 2 MiB,413。
在代码里,.layer(DefaultBodyLimit::max(..)) 写在 Router 末尾,紧挨着 axum::serve,是最不容易出错的位置;写在中间,后面加的路由都还是 2 MiB。
小结
- 「Failed to buffer the request body」来自 axum-core 的
LengthLimitError拒绝,「length limit exceeded」来自 http-body-util 的LengthLimitError;Bytes、String、Json、Form超限都是这一句,状态码 413。 - 默认上限 2 MiB,在提取器读 body 的过程中生效,不看
Content-Length:超限的请求会先被读到上限才被拒绝。 - 回完 413 服务端就关连接;先写完再读的客户端在大 body 时会在写上碰到
BrokenPipe/ConnectionReset,可能根本看不到 413。curl 这类边写边读的客户端能拿到 413。 - 放宽上限加在需要的路由上;加在
Router上时只影响已经存在的路由。disable()之后要配RequestBodyLimitLayer。