How do I handle multipart form uploads with Reqwest?
Handling multipart form uploads with Reqwest is essential for file uploads and complex form submissions in Rust applications. Reqwest provides built-in support for multipart forms through its multipart
module, making it straightforward to upload files, send form data, and handle various content types.
Setting Up Reqwest for Multipart Forms
First, ensure you have Reqwest with the multipart
feature enabled in your Cargo.toml
:
[dependencies]
reqwest = { version = "0.11", features = ["json", "multipart"] }
tokio = { version = "1.0", features = ["full"] }
Basic File Upload
Here's how to upload a single file using Reqwest:
use reqwest::multipart;
use std::fs::File;
use std::io::Read;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// Read file content
let mut file = File::open("example.txt")?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
// Create multipart form
let form = multipart::Form::new()
.part("file", multipart::Part::bytes(contents)
.file_name("example.txt")
.mime_str("text/plain")?);
// Send the request
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Status: {}", response.status());
println!("Response: {}", response.text().await?);
Ok(())
}
Uploading Multiple Files
When you need to upload multiple files, you can add multiple parts to the same form:
use reqwest::multipart;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// Create form with multiple files
let mut form = multipart::Form::new();
// Add first file
let file1_content = fs::read("document1.pdf")?;
form = form.part("document1", multipart::Part::bytes(file1_content)
.file_name("document1.pdf")
.mime_str("application/pdf")?);
// Add second file
let file2_content = fs::read("image.jpg")?;
form = form.part("image", multipart::Part::bytes(file2_content)
.file_name("image.jpg")
.mime_str("image/jpeg")?);
// Add text field
form = form.text("description", "Multiple file upload example");
let response = client
.post("https://httpbin.org/post")
.multipart(form)
.send()
.await?;
println!("Upload status: {}", response.status());
Ok(())
}
Mixed Form Data with Files
Often, you need to combine file uploads with regular form fields. Here's how to handle mixed multipart forms:
use reqwest::multipart;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// Read file
let file_content = std::fs::read("avatar.png")?;
// Create comprehensive form
let form = multipart::Form::new()
// Text fields
.text("username", "john_doe")
.text("email", "john@example.com")
.text("age", "30")
// JSON data as text
.text("metadata", json!({
"upload_date": "2024-01-15",
"category": "profile"
}).to_string())
// File upload
.part("avatar", multipart::Part::bytes(file_content)
.file_name("avatar.png")
.mime_str("image/png")?);
let response = client
.post("https://api.example.com/users")
.header("Authorization", "Bearer your-token-here")
.multipart(form)
.send()
.await?;
match response.status().is_success() {
true => println!("Upload successful!"),
false => println!("Upload failed: {}", response.status()),
}
Ok(())
}
Streaming File Uploads
For large files, you might want to stream the content instead of loading it entirely into memory:
use reqwest::multipart;
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// Open file for streaming
let file = File::open("large_file.zip").await?;
let stream = FramedRead::new(file, BytesCodec::new());
// Create streaming part
let file_part = multipart::Part::stream(reqwest::Body::wrap_stream(stream))
.file_name("large_file.zip")
.mime_str("application/zip")?;
let form = multipart::Form::new()
.text("upload_type", "backup")
.part("file", file_part);
let response = client
.post("https://api.example.com/upload")
.multipart(form)
.send()
.await?;
println!("Streaming upload completed: {}", response.status());
Ok(())
}
Error Handling and Validation
Proper error handling is crucial when working with file uploads:
use reqwest::multipart;
use std::path::Path;
async fn upload_with_validation(
file_path: &str,
upload_url: &str
) -> Result<String, Box<dyn std::error::Error>> {
// Validate file exists and size
let path = Path::new(file_path);
if !path.exists() {
return Err("File does not exist".into());
}
let metadata = std::fs::metadata(path)?;
let file_size = metadata.len();
// Check file size limit (10MB)
if file_size > 10 * 1024 * 1024 {
return Err("File too large (max 10MB)".into());
}
// Determine MIME type based on extension
let mime_type = match path.extension().and_then(|ext| ext.to_str()) {
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
Some("pdf") => "application/pdf",
Some("txt") => "text/plain",
_ => "application/octet-stream",
};
let client = reqwest::Client::new();
let file_content = std::fs::read(path)?;
let file_name = path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown");
let form = multipart::Form::new()
.part("file", multipart::Part::bytes(file_content)
.file_name(file_name)
.mime_str(mime_type)?);
let response = client
.post(upload_url)
.timeout(std::time::Duration::from_secs(30))
.multipart(form)
.send()
.await?;
if response.status().is_success() {
Ok(response.text().await?)
} else {
Err(format!("Upload failed with status: {}", response.status()).into())
}
}
#[tokio::main]
async fn main() {
match upload_with_validation("document.pdf", "https://api.example.com/upload").await {
Ok(response) => println!("Success: {}", response),
Err(e) => eprintln!("Error: {}", e),
}
}
Progress Tracking for Large Uploads
For better user experience with large file uploads, you can implement progress tracking:
use reqwest::multipart;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
struct ProgressTracker {
uploaded: Arc<AtomicU64>,
total: u64,
}
impl ProgressTracker {
fn new(total: u64) -> Self {
Self {
uploaded: Arc::new(AtomicU64::new(0)),
total,
}
}
fn progress(&self) -> f64 {
let uploaded = self.uploaded.load(Ordering::Relaxed);
(uploaded as f64 / self.total as f64) * 100.0
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let file_content = std::fs::read("large_video.mp4")?;
let total_size = file_content.len() as u64;
let tracker = ProgressTracker::new(total_size);
// In a real implementation, you'd wrap the content in a progress-tracking stream
let form = multipart::Form::new()
.text("title", "My Video Upload")
.part("video", multipart::Part::bytes(file_content)
.file_name("video.mp4")
.mime_str("video/mp4")?);
println!("Starting upload of {} bytes", total_size);
let response = client
.post("https://api.example.com/videos")
.multipart(form)
.send()
.await?;
println!("Upload completed with status: {}", response.status());
Ok(())
}
Integration with Web Scraping Workflows
When building web scraping applications, you might need to upload scraped files or submit forms with attachments. Here's how multipart uploads can integrate with scraping workflows:
use reqwest::multipart;
use scraper::{Html, Selector};
async fn scrape_and_upload_images(
source_url: &str,
upload_endpoint: &str
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
// Scrape the source page
let html_content = client.get(source_url).send().await?.text().await?;
let document = Html::parse_document(&html_content);
let img_selector = Selector::parse("img")?;
// Process each image
for img_element in document.select(&img_selector) {
if let Some(src) = img_element.value().attr("src") {
// Download image
let img_response = client.get(src).send().await?;
if img_response.status().is_success() {
let img_bytes = img_response.bytes().await?;
// Extract filename from URL
let filename = src.split('/').last().unwrap_or("image.jpg");
// Upload via multipart form
let form = multipart::Form::new()
.text("source_url", source_url)
.text("original_src", src)
.part("image", multipart::Part::bytes(img_bytes.to_vec())
.file_name(filename)
.mime_str("image/jpeg")?);
let upload_response = client
.post(upload_endpoint)
.multipart(form)
.send()
.await?;
println!("Uploaded {}: {}", filename, upload_response.status());
}
}
}
Ok(())
}
Best Practices and Tips
Memory Management
- For large files, prefer streaming uploads over loading entire files into memory
- Consider chunked uploads for very large files
- Monitor memory usage in production applications
Security Considerations
- Always validate file types and sizes before uploading
- Sanitize filenames to prevent path traversal attacks
- Use proper authentication and authorization
- Implement rate limiting for upload endpoints
Performance Optimization
- Use connection pooling for multiple uploads
- Implement retry logic with exponential backoff
- Consider parallel uploads for multiple small files
- Set appropriate timeouts based on file sizes
Content-Type Headers
Reqwest automatically sets the correct Content-Type
header for multipart forms, but you can customize individual parts:
let form = multipart::Form::new()
.part("data", multipart::Part::text("custom data")
.mime_str("application/json")?);
Troubleshooting Common Issues
Large File Timeouts: Increase the timeout duration for large file uploads:
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()?;
Memory Issues: Use streaming for large files instead of loading into memory.
MIME Type Detection: Implement proper MIME type detection based on file content, not just extensions.
Advanced Scenarios
Custom Part Headers
You can add custom headers to individual parts of your multipart form:
let custom_part = multipart::Part::bytes(file_content)
.file_name("data.json")
.mime_str("application/json")?;
let form = multipart::Form::new()
.part("data", custom_part);
Working with Different HTTP Methods
While most file uploads use POST, you can use multipart forms with other HTTP methods:
let response = client
.put("https://api.example.com/update")
.multipart(form)
.send()
.await?;
Handling Server Responses
Always check the server response for upload success and handle various status codes appropriately:
match response.status() {
reqwest::StatusCode::OK | reqwest::StatusCode::CREATED => {
println!("Upload successful");
},
reqwest::StatusCode::BAD_REQUEST => {
eprintln!("Bad request - check file format");
},
reqwest::StatusCode::PAYLOAD_TOO_LARGE => {
eprintln!("File too large");
},
_ => {
eprintln!("Upload failed: {}", response.status());
}
}
Multipart form uploads with Reqwest provide a robust foundation for file handling in Rust applications. Whether you're building web scrapers that need to upload collected data or APIs that handle file submissions, these patterns will help you implement reliable and efficient upload functionality. When handling timeouts in your applications, remember to account for larger files requiring longer upload times.