← 中文资源

简体中文版本

Axum 分段上传体积限额

在 Axum 接收 multipart 上传:请求体总量与单字段体积双重上限、MIME 白名单先于读取校验、字段流分块消费即时计数。

阅读英文原文

示例说明

上传接口的两道闸缺一不可: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();
}