The Form extractor automatically handles data from different sources based on the request method:
// Form data comes from query string// GET /search?q=axum&page=1#[derive(Deserialize)]struct SearchQuery { q: String, page: Option<u32>,}async fn search(Form(query): Form<SearchQuery>) -> String { format!("Searching for '{}' on page {}", query.q, query.page.unwrap_or(1))}
For POST requests, the Form extractor requires the Content-Type header to be application/x-www-form-urlencoded. Otherwise, it will reject the request with a 415 Unsupported Media Type error.
// Get entire field contents as byteswhile let Some(field) = multipart.next_field().await.unwrap() { let data = field.bytes().await.unwrap(); println!("Received {} bytes", data.len());}
use axum::extract::Multipart;use std::io::Write;use tokio::fs::File;use tokio::io::AsyncWriteExt;async fn save_file(mut multipart: Multipart) -> String { while let Some(field) = multipart.next_field().await.unwrap() { let file_name = field.file_name().unwrap().to_string(); let data = field.bytes().await.unwrap(); // Save to disk let path = format!("./uploads/{}", file_name); tokio::fs::write(&path, &data).await.unwrap(); return format!("Saved file: {file_name}"); } "No file uploaded".to_string()}
Always validate and sanitize file names before saving to prevent directory traversal attacks.
#[cfg(test)]mod tests { use super::*; use axum::{ body::Body, http::{self, Request, StatusCode}, }; use http_body_util::BodyExt; use tower::ServiceExt; #[tokio::test] async fn test_form_submission() { let app = Router::new() .route("/", post(accept_form)); let response = app .oneshot( Request::post("/") .header( http::header::CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(), ) .body(Body::from("username=john&email=john@example.com")) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); let body_str = std::str::from_utf8(&body).unwrap(); assert!(body_str.contains("john")); }}
Common form issues and solutions
Missing Content-Type header: POST forms require Content-Type: application/x-www-form-urlencoded. Check your client is sending this header.Form extractor must be last: If you have multiple extractors, Form and Multipart must come last since they consume the request body.Field name mismatch: Ensure your struct field names match the HTML form field names, or use serde’s #[serde(rename = "...")] attribute.Large file uploads fail: Increase the body size limit using DefaultBodyLimit and RequestBodyLimitLayer.