multipart - multipart/form-data build and parse
Import with import "multipart.j" as multipart;. Build and parse
multipart/form-data bodies (RFC 7578) - the file-upload counterpart to
mime’s email multipart. build turns a list of Parts (form
fields and files) into a (contentType, body) pair ready to POST; parse turns
a Content-Type header and a body back into parts. Bodies are bytes, so
binary file content round-trips intact. Pure .j over strings + bytes; runs
on both binaries.
import "multipart.j" as multipart;
use convert;
def parts as list of multipart.Part init [
multipart.field("title", "hello"),
multipart.file("doc", "a.txt", "text/plain", convert.bytesFromString("hi", "utf-8"))
];
def form as multipart.Built init multipart.build($parts);
# POST $form.body with header Content-Type: $form.contentType
def back as list of multipart.Part init multipart.parse($form.contentType, $form.body);
Runnable: examples/modules/multipart_demo.j.
Parts
def struct multipart.Part {
name as string, # the field name
filename as string, # the file name ("" for a plain field)
contentType as string, # the part Content-Type ("" for a plain field)
data as bytes # the part body
};
| Call | Returns | |
|---|---|---|
multipart.field(name, value) | Part | a plain text field |
multipart.file(name, filename, contentType, data) | Part | a file part (body is bytes) |
multipart.text(p) | string | decode a part’s body as UTF-8 (for field values) |
multipart.isFile(p) | bool | whether the part carries a filename |
Building
| Call | Returns | |
|---|---|---|
multipart.build(parts) | Built | encode with a fresh random boundary |
multipart.buildWith(parts, boundary) | Built | encode with an explicit boundary (deterministic) |
Built{ contentType, body } pairs the ready-to-send Content-Type header
(carrying the boundary) with the encoded body. Use build normally;
buildWith when you need a fixed boundary (tests, reproducibility). The boundary
must not occur inside any part body - the random one from build makes that
effectively impossible.
Parsing
multipart.parse(contentType, body) reads the boundary from the Content-Type
header and splits the body into Parts. It matches the delimiter only at
CRLF--boundary (normalising a leading boundary), so the boundary token
appearing inside a file body does not split it. A missing boundary or a part
without a header terminator throws Error{kind: "multipart"}.
def back as list of multipart.Part init multipart.parse($contentType, $body);
for (def p in $back) {
if (multipart.isFile($p)) {
# $p.filename, $p.contentType, $p.data
} else {
# multipart.text($p) is the field value
}
}
Scope
form-dataonly. The genericmultipart/*(mixed, alternative, related) used in email ismime’s job; this is the browser/HTTP upload shape.Content-Disposition/Content-Typeheaders only. Other per-part headers and RFC 2231 extended (name*=) parameter encoding are not parsed;name/filenameare read from the quotedContent-Dispositionparams.- No streaming. The whole body is built / parsed in memory - fine for form posts, not for multi-gigabyte uploads.
- Names / filenames are emitted verbatim inside quotes; avoid
"and CRLF in them (RFC 7578 percent-encoding is a follow-on).
See also
- mime.md - email-style multipart and header encoding.
- http.md / web.md - send an upload, or handle one.
- modules/index.md - the module catalog and import rules.