multipartkit/part
Types
A parsed multipart part.
Opaque — construct with new/5 (or receive from parser.parse) and
inspect through all_headers/1, name/1, filename/1,
content_type/1, and body/1. The case-insensitive
header(part, name) and headers(part, name) helpers below are the
supported way to look up a header value by name. The internal
layout may evolve to cache more derived fields without breaking
external callers.
Header semantics on the headers list:
- Entries are kept in the order they appeared on the wire.
- Header names retain their original casing.
- Header values have surrounding optional whitespace stripped per RFC 7230 §3.2.4 but are otherwise preserved (no quote unescaping, no parameter normalisation, no inner whitespace collapse).
Header bytes must be valid UTF-8 — header blocks that contain non-UTF-8
bytes are rejected with InvalidHeader. The body has no such
restriction: it is always treated as raw bytes; the parser does not
transcode or UTF-8-validate it.
name, filename, and content_type are convenience caches derived from
Content-Disposition and Content-Type headers per the field/file
detection rules. When new/5 is given non-None cache values without
the corresponding header entry in headers, the header is synthesised
so the cached value also appears on the wire (see new/5 for details).
pub opaque type Part
Values
pub fn all_headers(part: Part) -> List(#(String, String))
All headers as (name, value) pairs in input order.
pub fn content_type(part: Part) -> option.Option(String)
The Content-Type header value, or None if the part has no
Content-Type header.
pub fn equal_on_wire(a: Part, b: Part) -> Bool
Structural equality on the wire-level content of two Part values.
Compares the headers list (preserving order, with case-sensitive
name matching that mirrors RFC 7578 §4.2) and the body bytes. The
convenience cache fields — name, filename, content_type — are
intentionally ignored because they are derived from the headers
and may differ between a Part.new/5-constructed value (where the
caller passes the cache) and a parsed Part (where the parser
derives the cache from Content-Disposition / Content-Type).
Two Parts that equal_on_wire returns True for will encode
to the same bytes via multipartkit.encode/2 (modulo the
boundary string, which is supplied at encode time).
Use this for property-style round-trip tests where the caller
passes one Part shape into the encoder and gets another
(cache-derived) shape back from the parser.
pub fn filename(part: Part) -> option.Option(String)
The convenience filename field derived from Content-Disposition
for form-data parts, or None.
pub fn header(part: Part, name: String) -> option.Option(String)
Return the first header value whose name matches name ASCII
case-insensitively.
pub fn headers(part: Part, name: String) -> List(String)
Return all header values whose name matches name ASCII case-insensitively
in the order they appear in the part headers.
pub fn list_equal_on_wire(a: List(Part), b: List(Part)) -> Bool
equal_on_wire lifted over a pair of Part lists. Returns True
when the lists have the same length and every paired element
satisfies equal_on_wire.
pub fn name(part: Part) -> option.Option(String)
The convenience name field derived from Content-Disposition for
form-data parts, or None for non-form parts and parts without a
disposition header.
pub fn new(
headers headers: List(#(String, String)),
name name: option.Option(String),
filename filename: option.Option(String),
content_type content_type: option.Option(String),
body body: BitArray,
) -> Result(Part, error.MultipartError)
Construct a Part.
The name, filename, and content_type parameters double as wire
instructions: when one of them is Some(_) and the corresponding header
is absent from headers, the constructor synthesises the missing header
(matching the shape multipartkit/form.add_field / add_file would
emit) so the cached value also appears in the encoded wire image and
survives a multipartkit.encode |> multipartkit.parse round trip.
Synthesis rules:
name: Some(n)and noContent-Dispositionheader → prependsContent-Disposition: form-data; name="n"(with; filename=...appended whenfilenameis alsoSome(_), using the RFC 5987filename*=form for non-ASCII filenames).content_type: Some(ct)and noContent-Typeheader → prependsContent-Type: ct.filename: Some(_)withoutnamedoes not synthesise aContent-Disposition(RFC 7578 §4.2 requiresname); the value is stored as the cache only.- When the relevant header IS present in
headers, the constructor leaves the headers list untouched — the caller’s explicit header wins.
Header names and values are validated to prevent CRLF / NUL injection
into the encoded wire image. A header value that contains \r, \n,
or NUL would otherwise let an attacker who controls the value smuggle
additional header lines into the encoded part — the multipart variant
of CRLF response splitting (RFC 9110 §5.5 disallows these bytes in
field-value). Header names additionally cannot contain : (would
split the header at parse time). The same CRLF / NUL guard applies to
name, filename, and content_type because they may be promoted to
header values by the synthesis rules above. The constructor rejects
these inputs with Error(InvalidHeaderName(_)) /
Error(InvalidHeaderValue(_, _)) rather than silently emitting an
off-spec wire image.