Rust in Everything
基于 axum 0.8.9

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:

类型状态码正文前半句什么时候
MissingJsonContentType415Expected request with `Content-Type: application/json`Json:没有或不是 JSON 的 Content-Type
JsonSyntaxError400Failed to parse the request body as JSONJson:不是合法 JSON
JsonDataError422Failed to deserialize the JSON body into the target typeJson:合法 JSON,但对不上类型
InvalidFormContentType415Form requests must have `Content-Type: application/x-www-form-urlencoded`Form:非 GET 请求的 Content-Type 不对
FailedToDeserializeForm400Failed to deserialize formForm:GET / HEAD 时反序列化失败
FailedToDeserializeFormBody422Failed to deserialize form bodyForm:其他方法反序列化失败
LengthLimitError413Failed to buffer the request body读 body 超过上限
UnknownBodyError400Failed to buffer the request body读 body 时其他错误(比如连接断了)
InvalidUtf8400Request body didn't contain valid UTF-8String:不是 UTF-8
InvalidBoundary400Invalid `boundary` for `multipart/form-data` requestMultipart:取不到 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 照样读完。
📝NOTE
冷知识:2 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 上。两层都加时谁说了算,取决于谁最后往扩展里写。

❗IMPORTANT
动脑筋:外层关掉,内层限 1024,谁赢?
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,一次只能有一个字段。

💬 关联讨论

发起讨论
加载中...