Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

File Uploads

Handling file uploads is a common requirement. RustAPI provides a Multipart extractor to parse multipart/form-data requests.

See the runnable example: crates/rustapi-rs/examples/file_upload.rs

cargo run -p rustapi-rs --example file_upload
curl -X POST http://127.0.0.1:8080/upload -F "file=@./README.md"

Dependencies

[dependencies]
rustapi-rs = "0.1.551"
tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread"] }

Buffered Upload Example

RustAPI’s Multipart extractor buffers the entire request body into memory before parsing. It suits small to medium uploads; set explicit body limits for larger files.

use rustapi_rs::prelude::*;
use std::path::PathBuf;

#[derive(Serialize, Schema)]
struct UploadResponse {
    message: String,
    files: Vec<FileResult>,
}

#[derive(Serialize, Schema)]
struct FileResult {
    original_name: String,
    stored_name: String,
    content_type: String,
    size_bytes: usize,
}

#[rustapi_rs::post("/upload")]
async fn upload_handler(mut multipart: Multipart) -> Result<Json<UploadResponse>> {
    tokio::fs::create_dir_all("./uploads").await
        .map_err(|e| ApiError::internal(e.to_string()))?;

    let mut uploaded_files = Vec::new();

    while let Some(field) = multipart
        .next_field()
        .await
        .map_err(|_| ApiError::bad_request("Invalid multipart"))?
    {
        if !field.is_file() {
            continue;
        }

        let original_name = field.file_name().unwrap_or("unknown.bin").to_string();
        let content_type = field
            .content_type()
            .unwrap_or("application/octet-stream")
            .to_string();
        let safe_filename = format!("upload-{}", original_name);
        let data = field.bytes().await.map_err(|e| ApiError::internal(e.to_string()))?;
        let size_bytes = data.len();

        let path = PathBuf::from("./uploads").join(&safe_filename);
        tokio::fs::write(&path, &data).await.map_err(|e| ApiError::internal(e.to_string()))?;

        uploaded_files.push(FileResult {
            original_name,
            stored_name: safe_filename,
            content_type,
            size_bytes,
        });
    }

    Ok(Json(UploadResponse {
        message: "Upload successful".into(),
        files: uploaded_files,
    }))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    RustApi::new()
        .body_limit(50 * 1024 * 1024)
        .layer(BodyLimitLayer::new(50 * 1024 * 1024))
        .route("/upload", post(upload_handler))
        .docs("/docs")
        .run("127.0.0.1:8080")
        .await
}

Key Concepts

1. Buffering

RustAPI loads the entire multipart/form-data body into memory.

  • Pros: Simple API, easy to work with.
  • Cons: High memory usage for concurrent large uploads.
  • Mitigation: Set a reasonable BodyLimitLayer (e.g., 10MB–100MB).

2. Body Limits

The default request body limit is 1MB. Increase it for upload routes via .body_limit(...) and/or .layer(BodyLimitLayer::new(size)).

3. Security

  • Path Traversal: Never trust client-provided filenames. Generate safe names or sanitize strictly.
  • Content Type: Client-controlled; do not rely on it for execution checks.
  • Executable Permissions: Store uploads where script execution is disabled.

Testing with cURL

curl -X POST http://localhost:8080/upload \
  -F "file1=@./image.png" \
  -F "file2=@./document.pdf"

Response:

{
  "message": "Upload successful",
  "files": [
    {
      "original_name": "image.png",
      "stored_name": "upload-image.png",
      "content_type": "image/png",
      "size_bytes": 12345
    }
  ]
}