form-data
Async multipart/form-data streaming parser for Rust, implementing RFC 7578.
Repository Health
Technical Analysis
form-data is a Rust crate that parses multipart/form-data request bodies per RFC 7578, exposing an async Stream/AsyncRead interface for reading fields and file uploads with minimal buffering. It’s built for HTTP server code that needs to accept large file uploads without loading entire payloads into memory.
The crate ships both async and sync backends behind Cargo feature flags, plus a configurable Limits type (per-field, per-file, and whole-stream size caps) to defend HTTP endpoints against oversized or malicious multipart payloads. It’s designed to sit directly on top of body streams from hyper or similar low-level HTTP crates rather than a batteries-included web framework.
What You Get
- Streaming Field API - iterate multipart fields one at a time via
form.try_next()without holding the entire payload in memory. - AsyncRead/AsyncWrite integration - each
FieldimplementsAsyncRead, so it can be piped directly into a file or another writer withcopy/copy_to. - Configurable Limits - cap field name size, field size, file size, stream size, and part counts to guard against oversized multipart payloads.
- Sync and async feature flags - use the crate under a Tokio/futures async runtime, or in a blocking/sync context via the
syncfeature.
Common Use Cases
- File upload endpoints - stream uploaded files straight to disk in an HTTP server without buffering entire files in RAM.
- GraphQL multipart requests - parse the GraphQL multipart request spec, where file uploads are embedded alongside mutation variables.
- Large-payload ingestion - accept multi-hundred-megabyte uploads with bounded buffer sizes instead of loading the whole body at once.
- Custom HTTP servers - add multipart parsing to low-level servers built on
hyperortiny_httpthat don’t include their own form-data handling.
Under The Hood
Architecture
FormData<T> wraps a State<T> behind an Arc<Mutex<>>, and State drives an incremental parsing state machine (a Flag enum: Delimiting, Heading, Headed, Header, Next, Eof) over an internal BytesMut buffer, locating part boundaries with memchr rather than buffering the whole body. In async mode (src/async.rs), State<T> implements futures_util::Stream when the wrapped IO type is itself a byte stream, pulling more bytes only as the buffer is drained and yielding decoded Field values through FormData::try_next. Field holds an Option<Arc<Mutex<State<T>>>> back-reference so it can keep pulling bytes from the shared state via AsyncRead once yielded, and only one field is readable at a time, keeping consumption ordered. When the sync feature is used instead, src/sync.rs drives the same State through a blocking Iterator. This separates parsing (state.rs) from transport (async.rs/sync.rs) cleanly, so the IO layer is swappable.
Tech Stack Rust, edition 2021. Core dependencies are bytes (BytesMut/Bytes buffer management), http (header types), memchr (boundary scanning), tracing (instrumentation), thiserror (typed errors), and serde (Deserialize/Serialize on Limits). The async feature pulls in futures-util’s io module for AsyncRead/AsyncWrite/Stream, deliberately runtime-agnostic rather than tied to Tokio directly. Dev-dependencies cover the examples and tests: hyper 1.8 + hyper-util + http-body/http-body-util for the primary hyper example server, tiny_http for the sync example, tokio for the async test runtime, async-fs/tempfile for file I/O in tests, and tracing-subscriber for test logging. No web framework is a runtime dependency — the crate is meant to sit under any low-level HTTP body stream. CI (.github/workflows/CI.yml) runs cargo test across Linux/macOS/Windows on the nightly toolchain, clippy with pedantic lints denied as warnings, cargo fmt —check, and a docs build with -D warnings.
Code Quality Errors are explicit and typed via a thiserror Error enum (Stream, BoxError, InvalidHeader, InvalidContentDisposition, PayloadTooLarge, FileTooLarge, FieldTooLarge, PartsTooMany, FieldsTooMany, FilesTooMany, FieldNameTooLong, TryLockError) — the shared-state mutex is accessed via try_lock() and its failure is propagated as a Result rather than unwrapped. Tests live under tests/ as three integration binaries (form-data.rs and hyper-body.rs behind the async feature, tiny-body.rs behind sync) with fixture files covering edge cases such as empty fields, the GraphQL multipart request spec, filenames with spaces, and specific issue regressions. A fuzz/ directory targets the parser with fuzz testing. The crate root sets #![forbid(unsafe_code)] and #![deny(nonstandard_style)], and CI enforces clippy::pedantic plus rustfmt on every push — a strong quality bar for a crate this size, though there’s no visible coverage tooling.
API Design The public surface is small and direct: FormData::new/with_limits, form.try_next() (Stream) or an Iterator under sync, and Field exposes its fields (name, filename, content_type, headers, length, index) as plain public struct members rather than hiding them behind getters, keeping call sites terse. copy_to_file/copy_to/bytes/ignore give callers a menu of fast paths depending on whether a field should be discarded, buffered, or streamed to a file — the crate’s main practical value over hand-rolling multipart parsing on hyper/http, neither of which include forms support. Limits uses a fluent builder (Limits::default().file_size(…).stream_size(…)) and each limit maps to a specific error variant for precise handling. Documentation is doc-comment-driven per public item (missing_docs is warned on) with a full worked example embedded as a doctest in lib.rs, though there’s no separate guide beyond docs.rs, and using the crate well requires the caller to already understand HTTP body streams.