AxumMiddleTechnical

Как обрабатывать загрузку файлов в Axum с помощью multipart?

Используйте экстрактор Multipart из axum (фича multipart в Cargo.toml); итерируйтесь через next_field().await, читайте данные методом .bytes() или чанками через .chunk(), и ограничивайте размер через DefaultBodyLimit.

Загрузка файлов в Axum через multipart

Axum поддерживает multipart-загрузку через крейт axum::extract::Multipart, который автоматически парсит тело запроса с типом multipart/form-data. Для работы необходимо подключить фичу multipart в зависимости Axum.

Подключение зависимости

# Cargo.toml
[dependencies]
axum = { version = "0.7", features = ["multipart"] }
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
bytes = "1"

Базовый обработчик загрузки

use axum::{
    extract::Multipart,
    routing::post,
    Router,
    Json,
};
use std::path::PathBuf;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

async fn upload_handler(mut multipart: Multipart) -> Result<Json<serde_json::Value>, String> {
    let mut uploaded_files: Vec<String> = Vec::new();

    while let Some(field) = multipart.next_field().await.map_err(|e| e.to_string())? {
        let name = field.name().unwrap_or("unknown").to_string();
        let file_name = field
            .file_name()
            .map(|s| s.to_string())
            .unwrap_or_else(|| format!("{}_upload", name));

        // Считываем содержимое поля
        let data = field.bytes().await.map_err(|e| e.to_string())?;

        // Сохраняем файл
        let path = PathBuf::from("uploads").join(&file_name);
        let mut file = File::create(&path).await.map_err(|e| e.to_string())?;
        file.write_all(&data).await.map_err(|e| e.to_string())?;

        uploaded_files.push(file_name);
    }

    Ok(Json(serde_json::json!({ "uploaded": uploaded_files })))
}

#[tokio::main]
async fn main() {
    tokio::fs::create_dir_all("uploads").await.unwrap();

    let app = Router::new().route("/upload", post(upload_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Потоковая запись для больших файлов

Метод .bytes() загружает всё в память. Для больших файлов используйте .chunk() в цикле:

use futures::TryStreamExt;

async fn upload_stream(mut multipart: Multipart) -> Result<String, String> {
    while let Some(mut field) = multipart.next_field().await.map_err(|e| e.to_string())? {
        let file_name = field
            .file_name()
            .unwrap_or("file")
            .to_string();
        let path = PathBuf::from("uploads").join(&file_name);
        let mut file = File::create(&path).await.map_err(|e| e.to_string())?;

        // Читаем чанками, не загружая всё в RAM
        while let Some(chunk) = field.chunk().await.map_err(|e| e.to_string())? {
            file.write_all(&chunk).await.map_err(|e| e.to_string())?;
        }
    }
    Ok("OK".to_string())
}

Ограничение размера файла

По умолчанию Axum не ограничивает размер multipart-тела. Используйте слой DefaultBodyLimit:

use axum::extract::DefaultBodyLimit;

let app = Router::new()
    .route("/upload", post(upload_handler))
    .layer(DefaultBodyLimit::max(10 * 1024 * 1024)); // 10 MB

Подводные камни

  • Без фичи multipart в Cargo.toml тип Multipart недоступен — получите ошибку компиляции.
  • Метод .bytes() буферизует весь файл в памяти; для файлов более 1–5 MB используйте потоковое чтение через .chunk().
  • Отсутствие DefaultBodyLimit открывает вектор DoS-атаки через загрузку гигантских файлов.
  • Имя файла из field.file_name() приходит от клиента и не безопасно: всегда санируйте путь (проверяйте на .., абсолютные пути, недопустимые символы).
  • MIME-тип из field.content_type() тоже задаётся клиентом — не доверяйте ему, проверяйте сигнатуру файла (magic bytes).
  • Поле может быть текстовым (не файлом) — всегда проверяйте field.file_name() и обрабатывайте оба случая.
  • При параллельной загрузке нескольких файлов в одном запросе убедитесь, что генерируете уникальные имена, иначе файлы перезапишут друг друга.
  • Не вызывайте next_field() после того, как предыдущее поле не было полностью прочитано — это приводит к ошибке парсинга.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics