multipart

    A streaming multipart reader/writer library integrated with `moonbitlang/async`.

    multipart
    form-data
    Download zip
    Author
    Version
    0.2.2
    License
    Apache-2.0
    Last updated
    14 days ago
    Downloads
    11K

    #multipart

    A streaming multipart reader/writer library integrated with moonbitlang/async.

    Reader/Writer handle MIME multipart, Form/FormWriter handle multipart/form-data. Part bodies are streamed as raw bytes.

    #Examples

    Upload a field and a file through an HTTP client:

    ///|
    async fn upload(client : @http.Client, file : &@io.Reader) -> @http.Response {
    let form = @multipart.FormWriter(client)
    client.request(Post, "/upload", extra_headers={
    "Content-Type": form.content_type(),
    })
    form.write_field("description", "Release archive")
    form.write_file("archive", filename="release.zip", file)
    form.finish()
    client.end_request()
    }

    Read an upload using the request's Content-Type and copy its archive part:

    ///|
    async fn receive_archive(
    source : &@io.Reader,
    target : &@io.Writer,
    content_type : String,
    ) -> Unit {
    let (media_type, parameters) = @multipart.parse_header_value(content_type)
    guard @http.CaseInsensitiveString(media_type) == "multipart/form-data" else {
    raise Failure("expected multipart/form-data")
    }
    guard parameters.get("boundary") is Some(boundary) else {
    raise Failure("missing multipart boundary")
    }
    let form = @multipart.Form(source, boundary~)
    while form.next_part() is Some(part) {
    if part.name() == "archive" {
    target.write_reader(part)
    }
    }
    }

    next_part() discards unread content from the previous part. Process parts sequentially; finish() leaves the underlying writer open.

    For other multipart subtypes, use Reader/Writer with your own headers:

    ///|
    async fn copy_parts(source : &@io.Reader, target : &@io.Writer) -> Unit {
    let reader = @multipart.Reader(source, boundary="incoming")
    let writer = @multipart.Writer(target, boundary="outgoing")
    while reader.next_part() is Some((headers, body)) {
    writer.write_reader(headers~, body)
    }
    writer.finish()
    }

    Choose an output boundary that does not occur in the content. FormWriter creates a random boundary automatically.

    #Tests

    See fixtures for specification sources and snapshot tests.

    Callback

    type Callback = async (&
    Writer
    ) -> Unit

    MultipartError

    pub suberror MultipartError {
    MultipartError(String)
    } derive(
    Debug
    )

    Form

    type Form

    Streams the parts of a multipart/form-data body in their original order.

    Form::Form

    fn Form::Form(source : &
    Reader
    , boundary~ : String) -> Form raise MultipartError

    Form::next_part

    async fn Form::next_part(self : Form) -> FormPart?

    Advancing discards any unread bytes from the previous part. Bodies remain bytes: charset and Content-Transfer-Encoding are not decoded. Nested multipart bodies are exposed as parts and can be traversed with Reader.

    FormPart

    type FormPart

    impl Reader for FormPart

    FormPart::content_type

    fn FormPart::content_type(self : FormPart) -> String

    Returns the full Content-Type value, including any charset parameter. RFC 7578 §4.4 specifies text/plain when the header is absent.

    FormPart::filename

    fn FormPart::filename(self : FormPart) -> String?

    Returns the supplied filename. When saving a file, the caller must discard directory components and choose a suitable local name (RFC 7578 §4.2). Percent escapes remain literal because multipart/form-data does not identify whether a filename uses the optional percent-encoding convention.

    FormPart::name

    fn FormPart::name(self : FormPart) -> String

    FormWriter

    type FormWriter

    FormWriter::FormWriter

    fn FormWriter::FormWriter(target : &
    Writer
    , boundary? : String) -> FormWriter raise MultipartError

    Create a form writer. The boundary defaults to 192 bits of platform entropy, so that it cannot occur in the part bodies (RFC 2046 section 5.1.1). Pass one explicitly when the platform cannot provide secure randomness, or when the wire bytes must be reproducible; in that case it is the caller's responsibility that no part body contains it.

    FormWriter::boundary

    fn FormWriter::boundary(self : FormWriter) -> String

    The generated boundary, for constructing a matching reader or MIME header.

    FormWriter::content_type

    fn FormWriter::content_type(self : FormWriter) -> String

    The HTTP Content-Type for the generated form body.

    FormWriter::finish

    async fn FormWriter::finish(self : FormWriter) -> Unit

    FormWriter::write_field

    async fn FormWriter::write_field(self : FormWriter, name : String, value : String) -> Unit

    Write a UTF-8 text field, preserving the value's line endings.

    FormWriter::write_file

    async fn FormWriter::write_file(self : FormWriter, name : String, filename? : String, content_type? : String, file : &
    Reader
    ) -> Unit

    Stream a file without closing its reader. An unspecified content type uses application/octet-stream; filename is optional (RFC 7578 sections 4.2–4.4).

    FormWriter::write_json

    async fn FormWriter::write_json(self : FormWriter, name : String, value : Json) -> Unit

    Write a JSON field with Content-Type application/json.

    FormWriter::write_part

    async fn FormWriter::write_part(self : FormWriter, headers~ : Map[
    CaseInsensitiveString
    , String], callback : async (&
    Writer
    ) -> Unit) -> Unit

    Write a part with caller-supplied MIME headers.

    Reader

    type Reader

    A sequential, streaming MIME multipart reader (RFC 2046 section 5.1.1). The source must be limited to the enclosing message body.

    Reader::Reader

    fn Reader::Reader(source : &
    Reader
    , boundary~ : String) -> Reader raise MultipartError

    boundary is the decoded Content-Type parameter, without quotes or --.

    Reader::next_part

    Read the next part's headers and a reader limited to its body. Advancing drains the previous part; retained readers then return EOF. Preamble and epilogue are ignored. Bodies are returned without transfer decoding. Header folding is unfolded; repeated names are comma-joined as in @http.Headers. Calls on this reader and its current part must be made sequentially.

    Writer

    type Writer

    Writer::Writer

    fn Writer::Writer(target : &
    Writer
    , boundary~ : String) -> Writer raise MultipartError

    Create a multipart writer. The caller must choose a boundary that does not occur as a delimiter in any part (RFC 2046 section 5.1.1).

    Writer::finish

    async fn Writer::finish(self : Writer) -> Unit

    Write the closing delimiter without closing the underlying writer. Repeated calls have no effect.

    Writer::write_part

    async fn Writer::write_part(self : Writer, headers~ : Map[
    CaseInsensitiveString
    , String], callback : async (&
    Writer
    ) -> Unit) -> Unit

    Write a part without buffering its body. Call parts sequentially, and use the callback's writer only until the callback returns.

    Writer::write_reader

    Stream a reader's remaining bytes as one part, leaving both streams open.

    parse_header_value

    fn parse_header_value(value : String) -> (String, Map[
    CaseInsensitiveString
    , String]) raise MultipartError

    Parses an unfolded MIME field value, such as Content-Type or Content-Disposition. The main value is a token or type/subtype; parameter names are case-insensitive. Main values and parameter values retain their case. Comments are ignored and quoted pairs are unescaped; percent escapes and extended parameters stay raw. Grammar: RFC 2045 §5.1 and RFC 2183 §2.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io