Multipart
provides multipart/form-data encoding for HTTP requests (RFC 7578).
Encoding manually
(let [parts [(Multipart.text-part "name" "Carp")
(Multipart.file-part "upload" "test.txt"
"text/plain" "file contents")]
boundary (Multipart.boundary-for &parts)]
(Client.post url
{@"Content-Type" [(Multipart.content-type-header &boundary)]}
&(Multipart.encode &parts &boundary)))
Pick the boundary with Multipart.boundary-for, not with
Multipart.generate-boundary: only the former is checked against the parts,
which is what keeps a body that happens to contain the delimiter from
splitting the message.
Convenience function
(Client.post-multipart url {}
&[(Multipart.text-part "field" "value")])
boundary-for
(Fn [(Ref (Array Part) a)] String)
(boundary-for parts)
returns a boundary that occurs nowhere in parts, as RFC 2046 §5.1.1
requires of the delimiter.
Starts from Multipart.generate-boundary and, while the candidate still
occurs in some part name, filename, content type or body, appends the
bcharsnospace character that follows the fewest of those occurrences. Each
round therefore divides the occurrence count by 62, so the boundary stays far
inside the 70-character limit even for a payload built to defeat it. Every
round copies and scans each name, filename, content type and body, so a large
upload pays a full pass per round.
content-type-header
(Fn [(Ref String a)] String)
(content-type-header boundary)
returns the Content-Type header value for multipart/form-data with the given boundary.
encode
(Fn [(Ref (Array Part) a), (Ref String b)] String)
(encode parts boundary)
encodes an array of parts into a multipart/form-data body string using the given boundary (RFC 7578).
Pass a boundary from Multipart.boundary-for; encode cannot re-pick one,
because the caller has already committed to it in the Content-Type header.
A CR or LF in a part name, filename or content type would end the header line and let the rest of the value pose as headers or as a further part, so both are percent-encoded as %0D and %0A, following the same rule as HTML form submission. Quotes in a name or filename are backslash-escaped. Values without those characters are emitted unchanged.
file-part
(Fn [(Ref String a), (Ref String b), (Ref String c), (Ref String d)] Part)
(file-part name filename content-type data)
creates a file upload part with a filename and content type.
The data parameter is the raw file contents as a string.
generate-boundary
(Fn [] String)
(generate-boundary)
generates a boundary string for multipart encoding, using the current time for uniqueness.
The result is not checked against any payload, so a part that contains it is
encoded into a message the receiver splits in the wrong places. Prefer
Multipart.boundary-for, which rules that out.
parse
(Fn [(Ref String a), (Ref String b)] (Result (Array FormPart) String))
(parse body boundary)
decodes a multipart/form-data body with the given boundary
into its FormParts. Fails on the same bodies
parse-bytes does.
A Carp String ends at its first NUL byte, so a binary body — any PNG, PDF or
zip upload — is cut short before parsing even begins, and usually decodes to no
parts at all. Reach for parse-bytes whenever the body might
not be text.
parse-bytes
(Fn [(Ref (Array Byte) a), (Ref String b)] (Result (Array BinaryPart) String))
(parse-bytes body boundary)
decodes a multipart/form-data body with the given
boundary into its BinaryParts, reading the body as bytes
so that binary uploads survive intact.
Fails when the opening boundary delimiter is absent, when a part’s headers are
not terminated by a CRLF CRLF — RFC 7578 requires every part to carry a
Content-Disposition, so a part with no header block at all is malformed — and
when a part’s header region holds a NUL byte: header field lines are US-ASCII,
and reading one as a String would cut a filename short without saying so.
Any of these fails the whole body, not just the offending part.
text-part
(Fn [(Ref String a), (Ref String b)] Part)
(text-part name value)
creates a text form field part with the given name and value.