rust
55 lines · 10 steps
Streaming multipart uploads in Axum
An Axum handler streams each multipart field to a temp file and returns JSON metadata for every saved upload.
Explained by
highlit
1use axum::{extract::Multipart, http::StatusCode, Json};
2use serde::Serialize;
3use tokio::io::AsyncWriteExt;
4use uuid::Uuid;
5
6#[derive(Serialize)]
7pub struct UploadedFile {
8 id: Uuid,
9 original_name: String,
10 content_type: String,
11 bytes: usize,
12}
13
14pub async fn upload(mut multipart: Multipart) -> Result<Json<Vec<UploadedFile>>, (StatusCode, String)> {
15 let mut uploaded = Vec::new();
16
17 while let Some(mut field) = multipart
18 .next_field()
19 .await
20 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
21 {
22 if field.name() != Some("files") {
23 continue;
24 }
25
26 let id = Uuid::new_v4();
27 let original_name = field.file_name().unwrap_or("unnamed").to_owned();
28 let content_type = field
29 .content_type()
30 .unwrap_or("application/octet-stream")
31 .to_owned();
32
33 let path = std::env::temp_dir().join(id.to_string());
34 let mut file = tokio::fs::File::create(&path)
35 .await
36 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
37
38 let mut bytes = 0usize;
39 while let Some(chunk) = field
40 .chunk()
41 .await
42 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
43 {
44 bytes += chunk.len();
45 file.write_all(&chunk)
46 .await
47 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
48 }
49
50 file.flush().await.ok();
51 uploaded.push(UploadedFile { id, original_name, content_type, bytes });
52 }
53
54 Ok(Json(uploaded))
55}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Streaming file fields chunk by chunk keeps memory flat regardless of upload size.
- 2Mapping every fallible await into a (StatusCode, String) gives the handler uniform, client-friendly error responses.
- 3Generating a UUID filename avoids collisions and untrusted-path issues when persisting user uploads.
Related explainers
rust
use chrono::{DateTime, Duration, FixedOffset, Utc}; #[derive(Debug)] pub struct EventWindow {
Parsing and measuring time windows in Rust
datetime
error-handling
pattern-matching
Intermediate
7 steps
go
package feed import ( "encoding/json"
Streaming a JSON array in Go
streaming
json
decoder
Intermediate
6 steps
rust
use axum::{ extract::Path, http::{header::ACCEPT, HeaderMap, StatusCode}, response::{Html, IntoResponse, Json, Response},
Content negotiation in an Axum handler
content-negotiation
http-headers
serialization
Intermediate
8 steps
python
import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path
Parallel file downloads with a thread pool
concurrency
thread-pool
streaming-io
Intermediate
8 steps
rust
use std::collections::HashMap; #[derive(Debug, Clone)] struct Record {
Batched aggregation with fold in Rust
iterators
fold
hashmap
Intermediate
9 steps
go
package validate import ( "fmt"
Struct validation with reflection in Go
reflection
struct-tags
validation
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-multipart-uploads-in-axum-explained-rust-c1cc/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.