Rust in Everything
基于 axum 0.8.9

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 MiB10 次都是 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 一节)。

📝NOTE
冷知识:0.5 有一个看 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。

❗IMPORTANT
动脑筋:三个路由,哪个收得下 3 MiB?
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());
  }
}
三个路由各收一个 3 MiB 的 body,各打印什么状态码?答案在本页末尾。

动脑筋答案

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。

💬 关联讨论

发起讨论
加载中...