moonhttp

    moonhttp — the HTTP family for MoonBit: URI references and percent coding, conditional requests and entity-tags, range requests, HTTP/1.1, HTTP/2 and HTTP/3 framing, HPACK and QPACK header compression, WebSocket framing and its handshake, Server-Sent Events, cookies, data URLs, multipart bodies and media types, each a package of its own. Bytes in, events out; no sockets.

    http
    url
    etag
    http3
    websocket
    hpack
    qpack
    protocol
    moonbit
    Download zip
    Version
    0.12.1
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    305

    #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[:]) // the frames as written
    @sse.events(received[:]) // what an EventSource dispatches from them

    // A cookie set on the way out and read on the way back.
    @cookie.Cookie::new("sid", token, http_only=true, secure=true).encode()
    @cookie.get(request_header[:], "sid"[:])

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

    // A second request for something that has not changed.
    let etag = @conditional.hash(body[:], digest=@sha2.Hasher::new())
    @conditional.Ask::read(headers[:]).evaluate(verb="GET", etag~) // Go | Fresh | Failed

    Run moon run examples/tour for the whole surface in one go. The fourteen worked examples run a package each:

    moon run examples/04-ws-handshake moon run examples/10-http3-message moon run examples/05-ws-frame moon run examples/11-http3-conn moon run examples/06-qpack-primitives moon run examples/12-range moon run examples/07-qpack-field moon run examples/13-url moon run examples/08-qpack-dynamic moon run examples/14-dataurl moon run examples/09-http3-frames moon run examples/15-conditional

    #Packages

    PackageWhatSpecification
    sseServer-Sent Events, both directionsWHATWG HTML, the event-stream format
    mimemultipart/form-data and application/x-www-form-urlencodedRFC 7578, WHATWG URL §5.1
    urlA URI reference: taking one apart, putting it back, resolving a relative one, and percent coding by componentRFC 3986
    dataurlA representation written inside the reference to itRFC 2397
    mediaWhat a response says its body is, and under what name to save itRFC 9110 §8.3, RFC 6266
    rangeAsking for part of a representation: the fields, the arithmetic that resolves a range against a length, and the multi-range bodyRFC 9110 §14
    conditionalEntity-tags, the five precondition fields, and the order they are weighed inRFC 9110 §8.8, §13
    cookieCookie and Set-Cookie, each read and writtenRFC 6265, and its revision for SameSite
    wsWebSocket framing: opcodes, masking, fragment reassembly, close statusesRFC 6455 §5, §7.4.1
    upgradeThe WebSocket opening handshake, both sidesRFC 6455 §4
    huffmanThe Huffman code both header compressions useRFC 7541 Appendix B
    headerA header field: a name and a value, as octetsRFC 9110 §5
    hpackHTTP/2 header compression: both tables, both endsRFC 7541
    qpackHTTP/3 header compression: both tables, the field lines, both instruction streamsRFC 9204
    http1HTTP/1.1 messages: the request and status lines, field lines, body framing and the chunked codingRFC 9112
    http2HTTP/2 framing: the nine-octet header, all ten frame types, the connection prefaceRFC 9113
    http3HTTP/3 frames, their placement rules, settings, stream types and connectionRFC 9114

    #One vocabulary

    The same word means the same thing in every package here, and in every other package the organisation publishes. encode and decode are a pair, and parse is for a format with no symmetric writer. code is an enum's value on the wire, so a WebSocket close status is status and never code. A limit passed is Exceeded(limit~, got~), a predicate reads as an adjective or is named is_, and a refusal is Refused.

    #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.

    A client wants the second of sse's two readings. decode answers each frame as written; events answers what a browser's EventSource hands its listeners — no event for a frame without data, the type defaulting to message, and the id and reconnection delay carried from frame to frame as the stream's own state.

    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. events is read against the same streams, and each rule by which it differs from decode is checked by a test that turns red when the rule is removed.

    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 + that is a space in a form body and nowhere else.

    url is measured against the test set RFC 3986 prints for itself: all forty-two references of §5.4, the abnormal half included — a resolver that passes the normal examples and fails those is the usual kind of broken. Beside them, the components that are absent rather than empty, the colon that does not make a scheme, and the percent-coding edges.

    dataurl is measured against the four examples of RFC 2397 §4, the second of which is deliberately malformed, and against the two things strict refuses.

    conditional follows §13, which states rules rather than printing vectors: the comparison table of §8.8.3.2 entry for entry, then §13.2.2's steps one at a time, then against each other — the order is the part that gets got wrong. The content tag is pinned to SHA-256's vector for hello.

    hpack is measured against all twelve worked examples of RFC 7541 Appendix C, in both directions: the encoder reproduces the published blocks octet for octet and the decoder reproduces the published header lists, with the three requests of a series sharing one dynamic table and the three responses evicting under a 256-octet ceiling. huffman is measured against the coded strings those same blocks contain.

    http3 is measured section by section: every frame type round-trips, every placement rule of §7.2 is checked on each of the three stream kinds, the settings and the unidirectional stream types round-trip, and a request stream decodes into its pseudo-headers, its fields and its body.

    qpack is measured against the instruction vectors of RFC 9204 Appendix B and, section by section, against every representation §4.5 defines — indexed and post-base, relative and absolute, with a name reference and with a literal name.

    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

    Content negotiation, ranges, caching and conditional requests. They are planned in that order; the tracking list lives with the project.

    The variable-length integers are not here either. HTTP/3's frames are counted in QUIC's varint and both header compressions count in RFC 7541's prefixed integer; neither belongs to HTTP, and both are written in more than one protocol. They are moonvar, which this depends on and which depends on nothing.

    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.