示例说明
上传接口的两道闸缺一不可:DefaultBodyLimit 在框架层掐断超限请求体,单字段上限在读取过程中即时判定——分块累计字节数一旦越界立即返回 413,不再继续接收。MIME 白名单放在读取之前,不合规类型一个字节都不落盘。三个限额形成层次:总量、单文件、类型各司其职。
技术要点
- DefaultBodyLimit 兜底请求体总量
- 分块计数即时执行字段上限
- MIME 白名单先于任何读取
代码解读
upload 循环逐字段处理:先校验内容类型,再进入分块循环累计写入缓冲——每块检查单字段上限。tokio::fs::write 异步落盘避免阻塞运行时。返回 201 附带每个文件的字节数清单。字段名直接用作文件名是演示简化,生产必须改名防穿越。
使用建议
大文件应流式直写磁盘而非缓冲到内存。文件名规范化或服务端生成。并发上传配中间件限流保护磁盘带宽。
代码示例
use axum::{
extract::{DefaultBodyLimit, Multipart},
http::StatusCode,
routing::post,
Router,
};
use futures_util::StreamExt;
use std::path::PathBuf;
const MAX_BODY: usize = 20 * 1024 * 1024;
const MAX_FIELD: usize = 15 * 1024 * 1024;
const ALLOWED_MIME: [&str; 2] = ["image/png", "application/pdf"];
async fn upload(mut multipart: Multipart) -> Result<(StatusCode, String), StatusCode> {
let mut saved = Vec::new();
while let Some(field) = multipart
.next_field()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
{
let name = field.name().unwrap_or("file").to_string();
let mime = field.content_type().unwrap_or("").to_string();
if !ALLOWED_MIME.contains(&mime.as_str()) {
return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
let mut written: usize = 0;
let mut buffer: Vec<u8> = Vec::with_capacity(8 * 1024);
// 分块读取字段流:单字段上限即时检查
while let Some(chunk) = field
.chunk()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
{
written += chunk.len();
if written > MAX_FIELD {
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}
buffer.extend_from_slice(&chunk);
}
let path = PathBuf::from("uploads").join(name);
tokio::fs::write(&path, &buffer)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
saved.push(format!("{name}({written} bytes)"));
}
Ok((StatusCode::CREATED, saved.join(", ")))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/upload", post(upload))
.layer(DefaultBodyLimit::max(MAX_BODY));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}