Forms & File Uploads
rust-web-server provides two parsers for HTML form submissions, both living under rust_web_server::body:
FormUrlEncoded— forapplication/x-www-form-urlencodedbodies (plain text fields)FormMultipartData— formultipart/form-databodies (files and binary data)
URL-encoded forms
use rust_web_server::body::form_urlencoded::FormUrlEncoded;use rust_web_server::request::Request;use rust_web_server::response::Response;use rust_web_server::router::PathParams;use rust_web_server::server::ConnectionInfo;
fn handle_login( req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _state: &(),) -> Response { let fields = match FormUrlEncoded::parse(req.body.clone()) { Ok(map) => map, Err(e) => { // body was not valid UTF-8 let mut r = Response::new(); r.status_code = 400; return r; } };
let username = fields.get("username").map(String::as_str).unwrap_or(""); let password = fields.get("password").map(String::as_str).unwrap_or("");
// ... authenticate ... Response::new()}FormUrlEncoded::parse takes the raw body bytes (Vec<u8>) and returns Result<HashMap<String, String>, String>. Percent-encoding is decoded and ASCII control characters are stripped automatically.
To serialise a map back to URL-encoded format:
use std::collections::HashMap;use rust_web_server::body::form_urlencoded::FormUrlEncoded;
let mut map = HashMap::new();map.insert("q".to_string(), "hello world".to_string());let encoded = FormUrlEncoded::generate(map);// encoded == "q=hello+world" (or similar)Multipart form data
HTML file-upload forms use enctype="multipart/form-data". The body consists of multiple parts, each with its own headers and a binary body. The parts are separated by a boundary string that appears in the Content-Type header.
Extracting the boundary
use rust_web_server::body::multipart_form_data::FormMultipartData;use rust_web_server::header::Header;
fn get_boundary(req: &Request) -> Result<String, String> { let ct = req.get_header("content-type") .ok_or_else(|| "missing Content-Type".to_string())?;
FormMultipartData::extract_boundary(&ct.value)}extract_boundary splits the Content-Type value on boundary= and returns everything after it.
Parsing the body
use rust_web_server::body::multipart_form_data::{FormMultipartData, Part};use rust_web_server::request::Request;use rust_web_server::response::Response;use rust_web_server::router::PathParams;use rust_web_server::server::ConnectionInfo;
fn upload( req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _state: &(),) -> Response { // 1. Get boundary from Content-Type let content_type = req .get_header("content-type") .map(|h| h.value.clone()) .unwrap_or_default();
let boundary = match FormMultipartData::extract_boundary(&content_type) { Ok(b) => b, Err(_) => { let mut r = Response::new(); r.status_code = 400; return r; } };
// 2. Parse all parts let parts = match FormMultipartData::parse(&req.body, boundary) { Ok(p) => p, Err(_) => { let mut r = Response::new(); r.status_code = 400; return r; } };
// 3. Iterate over parts for part in &parts { // Each part has headers (e.g. Content-Disposition, Content-Type) if let Some(disposition) = part.get_header("content-disposition".to_string()) { let name = extract_field_name(&disposition.value); let file_name = extract_filename(&disposition.value);
if let Some(fname) = file_name { // This part is a file upload let file_bytes: &[u8] = &part.body; println!("Received file '{}' ({} bytes)", fname, file_bytes.len()); // Save file_bytes to disk, S3, etc. } else { // Plain text field let value = String::from_utf8_lossy(&part.body); println!("Field '{}' = '{}'", name, value); } } }
Response::new()}
fn extract_field_name(disposition: &str) -> &str { // Content-Disposition: form-data; name="field_name"; filename="file.txt" disposition .split(';') .find_map(|seg| seg.trim().strip_prefix("name=\"").and_then(|s| s.strip_suffix('"'))) .unwrap_or("")}
fn extract_filename(disposition: &str) -> Option<&str> { disposition .split(';') .find_map(|seg| seg.trim().strip_prefix("filename=\"").and_then(|s| s.strip_suffix('"')))}The Part type
Each Part returned by FormMultipartData::parse has:
| Field | Type | Description |
|---|---|---|
headers | Vec<Header> | Per-part headers (Content-Disposition, Content-Type, etc.) |
body | Vec<u8> | Raw bytes of this part’s body |
Call part.get_header("header-name") for case-insensitive lookup of a single header.
Size limits
Request bodies are limited by request_allocation_size from ConnectionInfo. The default is configured via RWS_CONFIG_REQUEST_ALLOCATION_SIZE. Large uploads beyond this limit are rejected at the TCP read stage before any parser is called.
Generating multipart bodies (testing)
FormMultipartData::generate can build a multipart body for outbound requests or tests:
use rust_web_server::body::multipart_form_data::{FormMultipartData, Part};use rust_web_server::header::Header;
let file_part = Part { headers: vec![ Header { name: "Content-Disposition".to_string(), value: r#"form-data; name="avatar"; filename="photo.jpg""#.to_string(), }, Header { name: "Content-Type".to_string(), value: "image/jpeg".to_string(), }, ], body: include_bytes!("photo.jpg").to_vec(),};
let boundary = "--WebKitFormBoundaryABC123";let body_bytes = FormMultipartData::generate(vec![file_part], boundary).unwrap();