Sign in

    moonhttp

    moonhttp — the HTTP family for MoonBit: WebSocket framing and its handshake, Server-Sent Events, multipart and URL-encoded bodies, and media types, each a package of its own. Bytes in, events out; no sockets.

    http
    websocket
    sse
    multipart
    mime
    protocol
    moonbit
    Download zip
    Version
    0.4.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    879

    #moonhttp

    The HTTP family for MoonBit: the formats a request and a response are written in, with nothing about sockets in them.

    // An event stream, one frame at a time.
    @sse.Event::new(data="{\"seq\":1}", kind="tick", id="1").encode()
    @sse.decode(received[:])

    // A posted form, whichever way the browser encoded it.
    let form = @mime.parse(body[:], content_type[:])
    form.field("name")
    form.file("avatar")

    Run moon run examples/tour for the whole surface in one go.

    #Packages

    PackageWhatSpecification
    sseServer-Sent Events, both directionsWHATWG HTML, the event-stream format
    mimemultipart/form-data and application/x-www-form-urlencoded, and percent-encoding both waysRFC 7578, WHATWG URL §5.1, RFC 3986
    mediaWhat a response says its body is, and under what name to save itRFC 9110 §8.3, RFC 6266
    wsWebSocket framing: opcodes, masking, fragment reassembly, close statusesRFC 6455 §5, §7.4.1
    upgradeThe WebSocket opening handshake, both sidesRFC 6455 §4

    #Configuration

    Every bound and every leniency is an argument, and every default is the one the mainstream uses.

    // A bound belongs to an endpoint. Either form names one.
    let small = @mime.Limits::new(parts=8, part_size=64 * 1024)
    let same = { ..@mime.limits, parts: 8, part_size: 64 * 1024 }

    @mime.parse(body[:], content_type[:], limits=small)

    // By default a body that is not a form is an empty form, and a multipart body
    // that stops making sense gives up the parts it read. `strict` says so instead.
    @mime.parse(body[:], content_type[:], strict=true) // raises NotAForm
    @mime.multipart(body[:], edge[:], strict=true) // raises Malformed(at~)

    @sse.Event::of("hello").encode(space=false) // drop the optional space

    SettingDefaultWhy that one
    limits.parts1000Starlette's max_files and max_fields
    limits.part_size1 MiBpython-multipart's in-memory part size
    strictoffA truncated upload is ordinary on a dropped connection, and "this request carried no form" is an answer rather than a failure. On is for the caller who would rather be told
    spaceonThe space after a colon is optional in the format and universal on the wire; a reader strips it either way

    Limits has no per-call mirror, and therefore no precedence rule. A bound belongs to an endpoint rather than to a request — an avatar upload and a spreadsheet import are two endpoints, each with its own — so the record is the only place it comes from and there is nothing to arbitrate. Where a setting can arrive from two places, as in mooncred, the precedence is published with it.

    #Downloads

    Naming a download is a question about a format, not about a framework: the answer is the same whether the bytes came from a file, a database or a generator. So it lives here rather than in whatever is serving them.

    @media.type_of("report.pdf") // "application/pdf"
    @media.disposition("my report.pdf") // filename*=utf-8''my%20report.pdf
    @media.disposition("cover.png", inline=true) // display it, do not save it

    A name that survives percent-encoding unchanged is quoted as it is; anything else — a space, an accent, a quote, a newline — goes out as RFC 6266's filename*, which is the only form that can carry a character outside ASCII and the only one a name cannot break out of.

    Text types carry charset=utf-8, because a browser handed text/plain with no charset applies its own locale's and shows something else.

    Still to come for a complete download: Range and Content-Range (RFC 9110 §14), which is what resumable downloads and media seeking need, and conditional requests. Reading the file and sending it in pieces is the server's, not this library's — there are no sockets here.

    #Framing, not transport

    Nothing here opens a connection or writes to one. sse turns an event into the bytes of a frame and back; sending each frame as its own chunk is the server's job, and it matters — a stream delivered as one body is not a stream, because a client dispatches an event only when it reads that event's blank line.

    mime reads a body that has already arrived. Both of its encodings are bounded by a Limits the caller sets, because the body came from whoever sent it: a million empty parts and one enormous part are two ways of asking a server to allocate more than it has. A form over its bounds is refused whole, not truncated — a handler given the first thousand parts of a larger form would be answering a request nobody sent.

    #What is checked

    sse is measured against the two example streams the WHATWG specification prints, which are the cases an implementation that trims too much or too little gets wrong; then every frame it writes is read back as the event that wrote it, the exact bytes of each frame are pinned, all three line endings are accepted, and the things a reader must ignore rather than refuse are ignored.

    mime is measured against a multipart/form-data body written the way a browser writes one — repeated names, a file with its own headers, the trailing CRLF that belongs to the delimiter and not the content — and against the percent-encoding edges, including a stray % that is not an escape and the + that is a space in a form body and nowhere else.

    ws is measured against all five frames RFC 6455 §5.7 prints: the unmasked and masked text messages, the fragmented one, the ping with its pong, and the two long length forms. upgrade is measured against the key and proof §1.3 prints, which is the one computation in it.

    #What is not here yet

    HTTP/1.1 message framing, HTTP/2 with HPACK, HTTP/3 with QPACK, content negotiation, cookies, ranges, caching and conditional requests. They are planned in that order; the tracking list lives with the project.

    TLS is moontls and QUIC is moonquic, so that parsing an HTTP message does not mean carrying a handshake. Compression is moonzip.

    #Install

    moon add moonbitstack/moonhttp

    #Licence

    Apache-2.0.