High-accuracy, low-allocation multipart/form-data parsing with a zero-copy Rust core and thin targets for Node.js and the browser.
crates/fastmulp_core: zero-copy parser and native APIcrates/fastmulp_napi: Node.js bindings built withnapi-rscrates/fastmulp_wasm: browser bindings built withwasm-bindgen
- The Rust core parses against a borrowed
&[u8]and returns body ranges instead of copying payload bytes. Content-Dispositionis parsed eagerly, andnameis enforced forform-dataparts.- Header storage uses
SmallVecso the common case stays stack-friendly. - Node.js and browser bindings return metadata plus
body_start/body_end, so callers can slice the original buffer themselves. - Boundary lines accept RFC 2046 transport padding, plus MIME-style preamble and epilogue.
- RFC 7578 Section 4.1: boundary handling
- RFC 7578 Section 4.2:
Content-Dispositionrequirements for each part - RFC 7578 Section 4.3: multiple files and older nested
multipart/mixed - RFC 2046 Section 5.1.1: multipart syntax, transport padding, preamble, epilogue
- HTML multipart/form-data algorithm: browser-side escaping rules for names and filenames
For deployed compatibility, fastmulp also accepts filename*= extended parameters even though RFC 7578 Section 4.2 says senders must not generate them.
filename values are untrusted input. Follow the security guidance in RFC 7578 Section 4.2 and avoid using path components blindly.
use fastmulp_core::parse;
let boundary = "demo-boundary";
let body = b"--demo-boundary\r\nContent-Disposition: form-data; name=\"field\"\r\n\r\nhello\r\n--demo-boundary--\r\n";
let multipart = parse(body, boundary.as_bytes())?;
let part = &multipart.parts()[0];
assert_eq!(part.name().and_then(|value| value.as_str().ok()), Some("field"));
assert_eq!(part.body(multipart.body()), b"hello");
# Ok::<(), fastmulp_core::Error>(())Node.js:
import { parse } from "./fastmulp.node";
const parts = parse(bodyBuffer, boundary);
const fileBytes = bodyBuffer.subarray(parts[0].body_start, parts[0].body_end);Browser:
import init, { parse } from "./fastmulp_wasm.js";
await init();
const parts = parse(formBytes, boundary);
const fieldBytes = formBytes.subarray(parts[0].body_start, parts[0].body_end);The wasm target still needs one JS-to-wasm copy at the ABI boundary, but it avoids extra copies after parsing by returning ranges instead of materialized part bodies.
Older nested multipart/mixed payloads can be handled by recursively calling parse on a part body after extracting the nested boundary from that part's Content-Type.
vp run release:patchvp run release:minorvp run release:alphavp run release:beta
Each release command updates the workspace version in Cargo.toml, creates a release commit, and creates the matching v... git tag. Tag pushes publish fastmulp-core through GitHub Actions trusted publishing.
vp run fmtvp run lintvp run checkvp run testincludes the release benchmark runvp run bench
fastmulp is licensed under GPL-3.0-or-later. See LICENSE and the GNU GPL v3 text.