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 serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 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.