body 提取器
主线位置:请求主线上
Handler::call的最后一个参数。前面的参数只拿Parts,最后一个参数拿到整个Request,可以消费 body(为什么只有最后一个能拿,见 FromRequestParts 与 FromRequest)。本页讲 axum 自带的几个消费 body 的提取器:它们怎么读、读多少、失败时回什么。
flowchart TB
req["Request(最后一个参数)"] --> ct{"Content-Type 对吗"}
ct -- 不对 --> r415["415"]
ct -- 对 --> lim["with_limited_body:查扩展"]
lim --> col["Limited 包住 body,collect"]
col -- 超过上限 --> r413["413"]
col -- 读完 --> de["serde_path_to_error 反序列化"]
de -- 语法错 --> r400["400"]
de -- 类型不对 --> r422["422"]
de -- 成功 --> ok["Json T 或 Form T"]
Json 和 Form 的骨架一样:先看请求头,不对就直接拒绝,body 一个字节都不读;对了就交给 Bytes 把整个 body 读进内存,读的时候受默认上限约束;最后反序列化。String 是 Bytes 加一次 UTF-8 检查。Multipart 不一样,它不读完 body,而是把 body 当成流交给 multer 边读边解析。
拒绝是怎么变成响应的
每种失败都是一个「rejection」类型。它们由两个宏生成:__define_rejection! 生成单个错误(带一个固定的状态码和一句固定的正文),__composite_rejection! 把几个单个错误合成一个枚举,比如 Json 的 JsonRejection 有 JsonDataError、JsonSyntaxError、MissingJsonContentType、BytesRejection 四个分支。
单个错误分两种。不带内部错误的,正文就是那句固定的话;带内部错误的(pub struct X(Error)),Display 是「固定的话 + : + 内部错误」,into_response 用的正是 body_text(),也就是这个 Display:
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str($body)?;
f.write_str(": ")?;
self.0.fmt(f)
}
}
所以客户端看到的正文是两段拼起来的:前半句来自 axum,后半句来自 serde_json 或 http-body-util。Handler::call 拿到 rejection 后直接 into_response() 返回,你的函数不会被调用(见 Handler trait)。
本页涉及的 rejection:
| 类型 | 状态码 | 正文前半句 | 什么时候 |
|---|---|---|---|
MissingJsonContentType | 415 | Expected request with `Content-Type: application/json` | Json:没有或不是 JSON 的 Content-Type |
JsonSyntaxError | 400 | Failed to parse the request body as JSON | Json:不是合法 JSON |
JsonDataError | 422 | Failed to deserialize the JSON body into the target type | Json:合法 JSON,但对不上类型 |
InvalidFormContentType | 415 | Form requests must have `Content-Type: application/x-www-form-urlencoded` | Form:非 GET 请求的 Content-Type 不对 |
FailedToDeserializeForm | 400 | Failed to deserialize form | Form:GET / HEAD 时反序列化失败 |
FailedToDeserializeFormBody | 422 | Failed to deserialize form body | Form:其他方法反序列化失败 |
LengthLimitError | 413 | Failed to buffer the request body | 读 body 超过上限 |
UnknownBodyError | 400 | Failed to buffer the request body | 读 body 时其他错误(比如连接断了) |
InvalidUtf8 | 400 | Request body didn't contain valid UTF-8 | String:不是 UTF-8 |
InvalidBoundary | 400 | Invalid `boundary` for `multipart/form-data` request | Multipart:取不到 boundary |
Json
Json<T> 的 FromRequest 只有三行:
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if !json_content_type(req.headers()) {
return Err(MissingJsonContentType.into());
}
let bytes = Bytes::from_request(req, state).await?;
Self::from_bytes(&bytes)
}
Content-Type 的判断
json_content_type 把头的值解析成 mime::Mime,然后要求主类型是 application,子类型是 json 或者后缀(+ 后面的部分)是 json:
let is_json_content_type = mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
于是 application/json; charset=utf-8(参数不影响)、application/vnd.api+json、application/problem+json 都算;没有这个头、值不是合法的 MIME、或者是 text/plain,都是 415。检查发生在读 body 之前,所以一个 Content-Type 不对的大请求不会被读进内存。
400 还是 422
Json::from_bytes 不直接调 serde_json::from_slice,而是用 serde_path_to_error 0.1.20 包一层,再按 serde_json 的错误分类分流:
fn make_rejection(err: serde_path_to_error::Error<serde_json::Error>) -> JsonRejection {
match err.inner().classify() {
serde_json::error::Category::Data => JsonDataError::from_err(err).into(),
serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
JsonSyntaxError::from_err(err).into()
}
// ...
}
}
let mut deserializer = serde_json::Deserializer::from_slice(bytes);
serde_path_to_error::deserialize(&mut deserializer)
.map_err(make_rejection)
.and_then(|value| {
deserializer
.end()
.map(|()| Self(value))
.map_err(|err| JsonSyntaxError::from_err(err).into())
})
Syntax和Eof(JSON 本身写错了、提前结束)回 400;Data(JSON 没错,但字段缺失、类型对不上、数值越界)回 422。区分它们是因为前者是「客户端发的不是 JSON」,后者是「发的是 JSON,但不是这个接口要的 JSON」。- 反序列化成功后还要调
deserializer.end():值后面还有非空白字符也是语法错误。这个错误没有经过 serde_path_to_error,所以正文里没有路径。 make_rejection和IntoResponse里的make_response一样,是函数里的一个非泛型内部函数,注释说「Extracted into separate fn so it's only compiled once for all T」:Json<T>对每个T单态化一次,把不依赖T的部分提出来,只编译一份。
serde_path_to_error 的 Display 在路径不是「全部未知」时先写路径再写原错误(items[0].qty: ...),路径为空时只写原错误。下面把这几种情况都跑一遍:
axum = { version = "=0.8.9", features = ["multipart"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "net", "time", "io-util"] }
futures = "0.3"
(本页和下面两页的示例都用这一组依赖,multipart 只有 Multipart 那一节需要。)
use axum::{Json, Router, body::Body, http::Request, routing::post};
use http_body_util::BodyExt;
use serde::Deserialize;
use tower::ServiceExt;
#[derive(Deserialize)]
#[allow(dead_code)]
struct Order {
id: u32,
items: Vec<Item>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct Item {
sku: String,
qty: u8,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", post(|Json(o): Json<Order>| async move { format!("ok, {} items", o.items.len()) }));
let good = r#"{"id":1,"items":[{"sku":"a","qty":2}]}"#;
let cases: [(&str, Option<&str>, &str); 10] = [
("no content-type", None, good),
("text/plain", Some("text/plain"), good),
("json+charset", Some("application/json; charset=utf-8"), good),
("+json suffix", Some("application/vnd.api+json"), good),
("upper case", Some("Application/JSON"), good),
("syntax error", Some("application/json"), r#"{"id":1,"items":["#),
("trailing data", Some("application/json"), r#"{"id":1,"items":[]} x"#),
("wrong type", Some("application/json"), r#"{"id":1,"items":[{"sku":"a","qty":300}]}"#),
("missing field", Some("application/json"), r#"{"id":1,"items":[{"sku":"a"}]}"#),
("top-level type", Some("application/json"), r#""hello""#),
];
for (name, ct, body) in cases {
let mut req = Request::post("/");
if let Some(ct) = ct {
req = req.header("content-type", ct);
}
let res = app.clone().oneshot(req.body(Body::from(body)).unwrap()).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
println!("{name:<16} {status} {}", String::from_utf8_lossy(&bytes));
}
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
no content-type 415 Unsupported Media Type Expected request with `Content-Type: application/json`
text/plain 415 Unsupported Media Type Expected request with `Content-Type: application/json`
json+charset 200 OK ok, 1 items
+json suffix 200 OK ok, 1 items
upper case 200 OK ok, 1 items
syntax error 400 Bad Request Failed to parse the request body as JSON: items: EOF while parsing a list at line 1 column 17
trailing data 400 Bad Request Failed to parse the request body as JSON: trailing characters at line 1 column 21
wrong type 422 Unprocessable Entity Failed to deserialize the JSON body into the target type: items[0].qty: invalid value: integer `300`, expected u8 at line 1 column 37
missing field 422 Unprocessable Entity Failed to deserialize the JSON body into the target type: items[0]: missing field `qty` at line 1 column 28
top-level type 422 Unprocessable Entity Failed to deserialize the JSON body into the target type: invalid type: string "hello", expected struct Order at line 1 column 7
- 大小写不同的
Application/JSON也通过:mime解析时把类型和子类型转成了小写。 syntax error那一行带着路径items:出错时 serde_path_to_error 已经走进了items这个字段,错误分类却是Eof,所以是 400 而不是 422。路径和状态码是两回事。wrong type、missing field的路径精确到数组下标;top-level type在根上就失败了,路径为空,正文里没有路径前缀。
Form 与 RawForm
Form<T> 先用 RawForm 拿到原始字节,再用 serde_urlencoded 0.7.1 反序列化,也套了 serde_path_to_error。RawForm 决定字节从哪来:
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if req.method() == Method::GET {
if let Some(query) = req.uri().query() {
return Ok(Self(Bytes::copy_from_slice(query.as_bytes())));
}
Ok(Self(Bytes::new()))
} else {
if !has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
return Err(InvalidFormContentType.into());
}
Ok(Self(Bytes::from_request(req, state).await?))
}
}
- GET 只看 query,body 完全不碰;没有 query 就当空表单。这对应浏览器提交
<form method="get">的方式。 - 其他方法只看 body,要求
Content-Type以application/x-www-form-urlencoded开头(has_content_type用的是starts_with,所以带; charset=utf-8也行)。这时 query 被忽略。
反序列化失败时,Form::from_request 按方法选 rejection:
let is_get_or_head =
req.method() == http::Method::GET || req.method() == http::Method::HEAD;
// ...
|err| -> FormRejection {
if is_get_or_head {
FailedToDeserializeForm::from_err(err).into()
} else {
FailedToDeserializeFormBody::from_err(err).into()
}
},
GET 的表单在 URL 里,坏了算请求本身有问题,回 400;body 里的表单坏了与 Json 的 Data 错误同类,回 422。
这里有一处不一致:Form 判断错误类型时把 HEAD 和 GET 算在一起,Form 的文档也写着「If the request has a method of GET or HEAD, the form data will be read from the query string」,可是 RawForm 只认 GET。HEAD 请求走进 else 分支,没有表单 Content-Type 就是 415。get(handler) 也会处理 HEAD(见 MethodRouter),所以这个差异是可以碰到的:
use axum::{Form, Router, body::Body, http::Request, routing::get};
use http_body_util::BodyExt;
use serde::Deserialize;
use tower::ServiceExt;
#[derive(Deserialize)]
struct Login {
user: String,
n: u32,
}
async fn handler(Form(l): Form<Login>) -> String {
format!("user={} n={}", l.user, l.n)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(handler).post(handler));
const FORM: &str = "application/x-www-form-urlencoded";
let cases: [(&str, &str, &str, Option<&str>, &str); 9] = [
("GET query", "GET", "/?user=ann&n=7", None, ""),
("GET bad n", "GET", "/?user=ann&n=x", None, ""),
("GET body only", "GET", "/", Some(FORM), "user=ann&n=7"),
("HEAD query", "HEAD", "/?user=ann&n=7", None, ""),
("POST body", "POST", "/", Some(FORM), "user=ann&n=7"),
("POST charset", "POST", "/", Some("application/x-www-form-urlencoded; charset=utf-8"), "user=ann&n=7"),
("POST bad n", "POST", "/", Some(FORM), "user=ann&n=x"),
("POST query only", "POST", "/?user=ann&n=7", None, ""),
("POST json ct", "POST", "/", Some("application/json"), "user=ann&n=7"),
];
for (name, method, uri, ct, body) in cases {
let mut req = Request::builder().method(method).uri(uri);
if let Some(ct) = ct {
req = req.header("content-type", ct);
}
let res = app.clone().oneshot(req.body(Body::from(body)).unwrap()).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
println!("{name:<16} {status} {}", String::from_utf8_lossy(&bytes));
}
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
GET query 200 OK user=ann n=7
GET bad n 400 Bad Request Failed to deserialize form: n: invalid digit found in string
GET body only 400 Bad Request Failed to deserialize form: missing field `user`
HEAD query 415 Unsupported Media Type
POST body 200 OK user=ann n=7
POST charset 200 OK user=ann n=7
POST bad n 422 Unprocessable Entity Failed to deserialize form body: n: invalid digit found in string
POST query only 415 Unsupported Media Type Form requests must have `Content-Type: application/x-www-form-urlencoded`
POST json ct 415 Unsupported Media Type Form requests must have `Content-Type: application/x-www-form-urlencoded`
GET body only:GET 带了表单 body 和正确的Content-Type,但 body 被忽略,query 为空,于是缺字段,400。HEAD query:同样的 query,GET 是 200,HEAD 是 415。响应正文是空的,因为 HEAD 的响应 body 被路由层去掉了,状态码还在。POST query only:POST 不看 query,没有Content-Type就是 415。
Bytes 与 String:上限在这里生效
Json、Form(经 RawForm)、String 最后都落到 axum-core 的 Bytes::from_request:
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let bytes = req
.into_limited_body()
.collect()
.await
.map_err(FailedToBufferBody::from_err)?
.to_bytes();
Ok(bytes)
}
String 在它后面加一次 String::from_utf8。关键是 into_limited_body,它调 with_limited_body:
fn with_limited_body(self) -> Request {
// update docs in `axum-core/src/extract/default_body_limit.rs` and
// `axum/src/docs/extract.md` if this changes
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
match self.extensions().get::<DefaultBodyLimitKind>().copied() {
Some(DefaultBodyLimitKind::Disable) => self,
Some(DefaultBodyLimitKind::Limit(limit)) => {
self.map(|b| Body::new(http_body_util::Limited::new(b, limit)))
}
None => self.map(|b| Body::new(http_body_util::Limited::new(b, DEFAULT_LIMIT))),
}
}
默认上限是 2_097_152 字节,即 2 MiB(注释写的是「2 mb」)。限制靠 http-body-util 的 Limited:每读到一帧,就从剩余额度里减掉;某一帧超出剩余额度时返回 LengthLimitError。Limited 不看 Content-Length,也不预先检查 size_hint,它的实现和报错原文见 http-body-util。
错误从 Limited 出来,被 Body::new 装了一层 axum_core::Error,collect 又装了一层,所以 FailedToBufferBody::from_err 要拆两层盒子,再 downcast 成 LengthLimitError:是它就 413,不是就当成 UnknownBodyError,400。
DefaultBodyLimit 只是一个扩展
DefaultBodyLimit 里只有一个 DefaultBodyLimitKind(Disable 或 Limit(usize))。作为 Layer,它生成的服务在 call 里只做一件事:
fn call(&mut self, mut req: Request<B>) -> Self::Future {
req.extensions_mut().insert(self.kind);
self.inner.call(req)
}
不读 body,不看 Content-Length,也不改响应;Future 就是内层的 S::Future,没有任何包装。真正的限制要等提取器调 with_limited_body 时才发生,而且只对调了它的提取器有效。with_limited_body 和 into_limited_body 是 RequestExt 的公开方法,自己写的 FromRequest 也可以调它们,用上同一个上限。axum-core 里调它的有 Bytes、BytesMut(以及间接调用的 String、Json、Form),axum 里还有 Multipart。直接拿 Request 或 Body 的提取器原样交出 body,不受这个上限约束。DefaultBodyLimit 自己的文档也这么写:「Note that if an extractor consumes the body directly with [Body::poll_frame], or similar, the default limit is not applied.」需要对所有请求一律限制,用 tower-http 的 RequestBodyLimitLayer(见 其他常用中间件)。
use axum::{
Router,
body::{Body, Bytes},
extract::{DefaultBodyLimit, Request},
routing::post,
};
use futures::StreamExt;
use http_body_util::BodyExt;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use tower::ServiceExt;
async fn send(app: &Router, name: &str, body: Body) {
let req = Request::post("/").body(body).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
println!("{name:<24} {status} {}", String::from_utf8_lossy(&bytes));
}
#[tokio::main]
async fn main() {
const LIMIT: usize = 2_097_152;
let bytes_app = Router::new().route("/", post(|b: Bytes| async move { format!("got {}", b.len()) }));
send(&bytes_app, "Bytes, 2097152", Body::from(vec![b'a'; LIMIT])).await;
send(&bytes_app, "Bytes, 2097153", Body::from(vec![b'a'; LIMIT + 1])).await;
// stream 64 KiB chunks and count how many the extractor pulls
let pulled = Arc::new(AtomicUsize::new(0));
let counter = pulled.clone();
let chunks = futures::stream::iter(0..100).map(move |_| {
counter.fetch_add(1, Ordering::SeqCst);
Ok::<_, std::io::Error>(Bytes::from(vec![b'a'; 64 * 1024]))
});
send(&bytes_app, "Bytes, 100 x 64 KiB", Body::from_stream(chunks)).await;
println!("{:<24} chunks pulled: {}", "", pulled.load(Ordering::SeqCst));
let disabled = bytes_app.clone().layer(DefaultBodyLimit::disable());
send(&disabled, "disable(), 3 MiB", Body::from(vec![b'a'; 3 << 20])).await;
let small = bytes_app.clone().layer(DefaultBodyLimit::max(1024));
send(&small, "max(1024), 1025", Body::from(vec![b'a'; 1025])).await;
let raw = Router::new().route(
"/",
post(|req: Request| async move {
let b = req.into_body().collect().await.unwrap().to_bytes();
format!("got {}", b.len())
}),
);
send(&raw, "Request, 3 MiB", Body::from(vec![b'a'; 3 << 20])).await;
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
Bytes, 2097152 200 OK got 2097152
Bytes, 2097153 413 Payload Too Large Failed to buffer the request body: length limit exceeded
Bytes, 100 x 64 KiB 413 Payload Too Large Failed to buffer the request body: length limit exceeded
chunks pulled: 33
disable(), 3 MiB 200 OK got 3145728
max(1024), 1025 413 Payload Too Large Failed to buffer the request body: length limit exceeded
Request, 3 MiB 200 OK got 3145728
- 恰好
2_097_152字节可以,多 1 字节就是 413。正文前半句是LengthLimitError的固定文字,后半句length limit exceeded是 http-body-util 的LengthLimitError的Display。 - 流式 body 每块 64 KiB:前 32 块正好 2 MiB,第 33 块超额,
Limited返回错误,collect停下。总共只拉了 33 块,剩下 67 块没有被读。限制是在读的过程中生效的,不会先把整个 body 读完再判断。 - 拿
Request自己collect,3 MiB 照样读完。
axum 最初的 Bytes::from_request 直接把整个 body 读进内存。#1346「Limit size of request bodies in Bytes extractor」的说明写道:「We recently received a report of a vulnerability in axum caused by Bytes::from_request calling hyper::body::into_bytes directly without setting a limit. This meant if someone sent an infinite request body the server would attempt to buffer the whole thing in memory and eventually OOM.」解决办法是「wrapping the body in http_body::Limited before calling into_bytes. This can be disabled by adding the new DefaultBodyLimit::disable() middleware. That sets a private extension which Bytes::from_request looks for.」
这就是今天 DefaultBodyLimitKind 这个私有扩展的来历。主线上它最早出现在 axum 0.6.0-rc.2(axum-core 0.3.0-rc.2);同一天回移到了 0.5 系列,CHANGELOG 把它列在 axum 0.5.16 的「Security」下,标为 breaking:「The default limit is at 2 MB and can be disabled by adding the new DefaultBodyLimit::disable() middleware.」
嵌套的 DefaultBodyLimit
DefaultBodyLimit::max 和 disable 都可以加在 Router 上,也可以加在单个路由的 MethodRouter 上。两层都加时谁说了算,取决于谁最后往扩展里写。
use axum::{Router, body::{Body, Bytes}, extract::{DefaultBodyLimit, Request}, routing::post};
use tower::ServiceExt;
async fn len(b: Bytes) -> String {
b.len().to_string()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/a", post(len).layer(DefaultBodyLimit::max(1024)))
.route("/b", post(len))
.layer(DefaultBodyLimit::disable());
for path in ["/a", "/b"] {
let req = Request::post(path).body(Body::from(vec![0u8; 4096])).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
println!("{path}: {}", res.status());
}
}
向 /a 和 /b 各发 4096 字节,两行各打印什么状态码?
Multipart
Multipart(要开 multipart feature)包着 multer 3.1.0 的 multer::Multipart。它的 FromRequest 只从 Content-Type 里取出 boundary,然后把(同样套上 Limited 的)body 变成数据流交给 multer,不读任何数据:
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let boundary = content_type_str(req.headers())
.and_then(|content_type| multer::parse_boundary(content_type).ok())
.ok_or(InvalidBoundary)?;
let stream = req.with_limited_body().into_body();
let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
Ok(Self { inner: multipart })
}
所以提取 Multipart 只会因为 boundary 失败(400)。读字段时的错误是 MultipartError,由你的 handler 用 ? 返回,它的状态码由 status_code_from_multer_error 决定:格式问题 400,超过 multer 自己的大小限制或 body 上限 413,其余 500。上限错误埋在 multer 的 StreamReadFailed 里,这个函数要逐层 downcast 才能找到 LengthLimitError;axum 0.8.9 起(#3611,CHANGELOG:「Return specific error message when multipart body limit is exceeded」)这种情况的正文是固定的 Request payload is too large。
Field 里多存了一个 &'a mut Multipart,没有任何运行时用途:
pub struct Field<'a> {
inner: multer::Field<'static>,
// multer requires there to only be one live `multer::Field` at any point. This enforces that
// statically, which multer does not do, it returns an error instead.
_multipart: &'a mut Multipart,
}
multer 要求同一时刻只能有一个活着的字段,违反时运行期报错;axum 借一个 &mut,让「上一个字段还没丢就要下一个」变成编译错误。
use axum::{Router, body::Body, extract::{DefaultBodyLimit, Multipart, Request}, routing::post};
use http_body_util::BodyExt;
use tower::ServiceExt;
async fn upload(mut mp: Multipart) -> Result<String, axum::extract::multipart::MultipartError> {
let mut out = String::new();
while let Some(field) = mp.next_field().await? {
let name = field.name().unwrap_or("").to_owned();
let file = field.file_name().map(str::to_owned);
let data = field.bytes().await?;
out += &format!("{name} {file:?} {} bytes\n", data.len());
}
Ok(out)
}
fn body(file_len: usize) -> String {
format!(
"--XyZ\r\ncontent-disposition: form-data; name=\"title\"\r\n\r\nhello\r\n\
--XyZ\r\ncontent-disposition: form-data; name=\"file\"; filename=\"a.bin\"\r\n\r\n{}\r\n\
--XyZ--\r\n",
"a".repeat(file_len)
)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", post(upload)).layer(DefaultBodyLimit::max(4096));
let cases = [
("ok", Some("multipart/form-data; boundary=XyZ"), body(1000)),
("no boundary", Some("multipart/form-data"), body(1000)),
("over limit", Some("multipart/form-data; boundary=XyZ"), body(5000)),
];
for (name, ct, b) in cases {
let mut req = Request::post("/");
if let Some(ct) = ct {
req = req.header("content-type", ct);
}
let res = app.clone().oneshot(req.body(Body::from(b)).unwrap()).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
println!("{name:<12} {status} {}", String::from_utf8_lossy(&bytes).trim_end());
}
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
ok 200 OK title None 5 bytes
file Some("a.bin") 1000 bytes
no boundary 400 Bad Request Invalid `boundary` for `multipart/form-data` request
over limit 413 Payload Too Large Request payload is too large
第三行:上限设成 4096,文件字段 5000 字节。Multipart 本身提取成功,field.bytes() 读到一半超额,? 把 MultipartError 交给 Result 的 IntoResponse,回 413。
动脑筋答案
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
/a: 413 Payload Too Large
/b: 200 OK
/a:请求先经过Router上的DefaultBodyLimit::disable(),扩展里写入Disable;再经过路由上的DefaultBodyLimit::max(1024),Extensions::insert同类型覆盖,扩展变成Limit(1024)。Bytes读的时候只看最后的值,4096 字节超额,413。/b:只有外层的Disable,不限,200。
越靠近 handler 的那层越晚执行,也就越晚写扩展,所以内层的设置总是赢,和两层谁更严格无关。
小结
- rejection 由
__define_rejection!/__composite_rejection!生成:状态码和前半句正文固定,带内部错误的再拼上:和内部错误的Display。 Json先查Content-Type(application/json或+json后缀,否则 415),再读 body,再用 serde_path_to_error 反序列化:语法错 400,类型错 422,正文里带出错路径。Form通过RawForm取字节:只有 GET 读 query,其他方法读 body 并要求表单Content-Type。反序列化失败时 GET / HEAD 回 400,其余 422;HEAD 实际走的是读 body 的分支。- 上限在
Bytes::from_request读 body 时生效:with_limited_body查扩展,默认用Limited限2_097_152字节,超出 413。DefaultBodyLimit只往请求里写扩展,内层覆盖外层;直接拿Request/Body不受限制。 Multipart只在提取时检查 boundary,读字段时才读 body;Field借着&mut Multipart,一次只能有一个字段。