HTTP formats for MoonBit with no I/O: HTTP/1 to HTTP/3, WebSocket, SSE and multipart.
// An event stream: the frames as written, and the events an EventSource
// dispatches from them.
let received = "id: 1\nevent: tick\ndata: {\"seq\":1}\n\n"
let events = @sse.events(received)
assert_eq(events[0].data, "{\"seq\":1}")
assert_eq(events[0].kind, Some("tick"))
assert_eq(
@sse.Event::new(data="{\"seq\":1}", kind="tick", id="1").encode(),
"id: 1\nevent: tick\ndata: {\"seq\":1}\n\n",
)
// A cookie set on the way out, and read on the way back.
assert_eq(
@cookie.Cookie::new("sid", "s3cr3t", http_only=true, secure=true).encode(),
"sid=s3cr3t; Path=/; Secure; HttpOnly; SameSite=Lax",
)
assert_eq(@cookie.get("sid=s3cr3t; theme=dark", "theme"), Some("dark"))
// A posted form, whichever way the browser encoded it.
let form = @form.parse(
b"name=ada&lang=mo"[:],
"application/x-www-form-urlencoded"[:],
)
assert_eq(form.field("name"), Some("ada"))
// A second request for something that has not changed.
let etag = @conditional.hash(b"hello"[:], digest=@sha2.Hasher::new())
let ask = @conditional.Ask::read([("If-None-Match", etag.encode())])
assert_eq(ask.evaluate(verb="GET", etag~), @conditional.Verdict::Fresh)// A frame is a byte layout: two header octets, a mask key, then the payload.
let frame : @ws.Frame = { fin: true, opcode: @ws.Opcode::Text, payload: b"hi" }
assert_eq(
frame.encode(mask=b"\x01\x02\x03\x04"),
b"\x81\x82\x01\x02\x03\x04\x69\x6b",
)
// A message compressed the way RFC 7692 asks, and the bit that says so.
let terms = @pmd.Extension::agree(
@pmd.offers("permessage-deflate; client_max_window_bits"[:])[:],
)
assert_eq(terms is Some(_), true)
let (wire, compressed) = @pmd.Writer::new(extension=terms.unwrap()).frame(
@ws.Opcode::Text,
b"Hello"[:],
)
assert_eq(compressed, true)
assert_eq(wire.payload, b"\xf2H\xcd\xc9\xc9\x07\x00")
// The same WebSocket over HTTP/3: the extended CONNECT of RFC 9220, allowed by
// one setting the server sends and answered with 200 or 501.
let connect : @http3.Request = {
meth: "CONNECT",
scheme: "https",
authority: "example.com",
path: "/chat",
protocol: "websocket",
headers: [@header.Header::of("sec-websocket-version", "13")],
body: b"",
}
let settings = @http3.Settings::new(
4096, 16384, 100, enable_connect_protocol=true,
)
assert_eq(@http3.Upgrade::of(connect, settings~) is @http3.Upgrade::Accept, true)
assert_eq(@http3.websocket_response(connect, settings~).status, "200")// A stored response, and what may be done with it.
let sent = @moondate.http("Sun, 06 Nov 1994 08:49:37 GMT")
let stored = @cache.Entry::read(
status=200,
headers=[("Cache-Control", "max-age=60")],
request=[],
sent~,
received=sent,
)
assert_eq(stored.lifetime(), Some(60))
// And where a 302 sends the request next, with which method.
let from = @moonurl.Url::decode("https://example.com/a") catch {
_ => abort("not a URL")
}
let step = @client.follow(
status=302,
location="/next",
from~,
verb=@method.Method::Post,
visited=[from][:],
)
assert_eq(step.url.encode(), "https://example.com/next")
assert_eq(step.verb, @method.Method::Get)moon run src/examples/04-ws-handshake moon run src/examples/09-http3-frames
moon run src/examples/05-ws-frame moon run src/examples/10-http3-message
moon run src/examples/06-qpack-primitives moon run src/examples/11-http3-conn
moon run src/examples/07-qpack-field moon run src/examples/12-range
moon run src/examples/08-qpack-dynamic moon run src/examples/13-conditional
moon run src/examples/14-ws-compressed moon run src/examples/15-cache| Package | What | Specification |
|---|---|---|
| sse | Server-Sent Events, both directions | WHATWG HTML, the event-stream format |
| form | A request body read as the form its Content-Type says it is, multipart/form-data or application/x-www-form-urlencoded | WHATWG HTML form submission |
| range | Asking for part of a representation: the fields, the arithmetic that resolves a range against a length, and the multi-range body | RFC 9110 §14 |
| conditional | Entity-tags, the five precondition fields, and the order they are weighed in | RFC 9110 §8.8, §13 |
| cache | What may be stored, how long it stays fresh, how old it is, and whether it may answer: the decisions, with no store | RFC 9111 |
| client | Where a redirect sends a request, with which method and which credentials; and Alt-Svc read into the alternatives it names | RFC 9110 §15.4, RFC 7838 |
| cookie | Cookie and Set-Cookie, each read and written | RFC 6265, and its revision for SameSite |
| ws | WebSocket framing: opcodes, masking, fragment reassembly, close statuses | RFC 6455 §5, §7.4.1 |
| pmd | The permessage-deflate extension: its four parameters, the offer and the answer, and a reader and writer over ws frames | RFC 7692 |
| deflate | Raw DEFLATE shaped for a stream that outlives a message: a window carried across messages, and a ceiling on what one may expand to | RFC 1951 |
| upgrade | The WebSocket opening handshake, both sides | RFC 6455 §4 |
| huffman | The Huffman code both header compressions use | RFC 7541 Appendix B |
| method | The request method: the nine standard ones and any other token, with whether each is safe and idempotent | RFC 9110 §9, RFC 5789 |
| header | A header field: a name and a value, as octets | RFC 9110 §5 |
| hpack | HTTP/2 header compression: both tables, both ends, and a bound on the list a block decodes to | RFC 7541 |
| qpack | HTTP/3 header compression: both tables, the field lines, both instruction streams | RFC 9204 |
| http1 | HTTP/1.1 messages: the request and status lines, field lines, body framing and the chunked coding; Conn, a whole connection for either end on h11's state machine, octets in and events out | RFC 9112 |
| http2 | HTTP/2 framing: the nine-octet header, all ten frame types, the connection preface; Conn, a whole connection for either end after python-hyper h2, with stream states, flow control, settings, push and field validation | RFC 9113, RFC 8441 |
| http3 | HTTP/3 frames, their placement rules, settings, stream types and connection; the connection error each refusal closes with; a reader for a request stream and one for each unidirectional stream; and WebSocket over HTTP/3 as the extended CONNECT RFC 9220 defines | RFC 9114, RFC 9220 |
// A bound belongs to an endpoint. Either form names one.
let body = b"name=ada&lang=mo"[:]
let content_type = "application/x-www-form-urlencoded"
let small = @moonmime.Limits::new(parts=8, part_size=64 * 1024)
let widened = { ..@moonmime.limits, parts: 8 }
assert_eq(small.parts, widened.parts)
let limited = @form.parse(body, content_type, limits=small)
assert_eq(limited.field("lang"), Some("mo"))
// 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.
let asked = @form.parse(body, content_type, strict=true) catch {
_ => abort("this body is a form")
}
assert_eq(asked.field("name"), Some("ada"))
// The space after the colon is optional in the format and universal on the wire.
assert_eq(@sse.Event::of("hello").encode(), "data: hello\n\n")
assert_eq(@sse.Event::of("hello").encode(space=false), "data:hello\n\n")
// A writer can leave a message under this many octets alone, which is worth
// doing: a compressed "OK" is longer than "OK".
let (short, compressed) = @pmd.Writer::new(threshold=64).frame(
@ws.Opcode::Text,
b"OK"[:],
)
assert_eq(compressed, false)
assert_eq(short.payload, b"OK")| Setting | Default | Why that one |
|---|---|---|
| limits.parts | 1000 | Starlette's max_files and max_fields |
| limits.part_size | 1 MiB | python-multipart's in-memory part size |
| strict | off | A 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 |
| space | on | The space after a colon is optional in the format and universal on the wire; a reader strips it either way |
| limit (pmd) | 16 MiB | The ceiling ws puts on a message anyway. RFC 7692 sets none, and a compressed message is where that absence would cost the most |
| threshold (pmd) | 0 | Every message compressed, which is what agreeing to the extension means. A caller compressing a bulletin board and not a chat box raises it |
// A DATA frame where a request stream still owes its HEADERS.
let wire = b"\x00\x04body"
let reader = @http3.Incoming::new(limit=16384) // SETTINGS_MAX_FIELD_SECTION_SIZE
let mut closed : @http3.Code? = None
let parts = reader.feed(wire) catch {
e => {
// e.code() is what the connection closes with; e.to_string() names the rule.
closed = Some(e.code())
[]
}
}
assert_eq(closed, Some(@http3.FrameUnexpected))
assert_eq(parts.length(), 0)| Code | Number | Raised when |
|---|---|---|
| NoError | 0x100 | the connection ends and nothing went wrong |
| GeneralProtocolError | 0x101 | a fault no other code names |
| InternalError | 0x102 | the endpoint hit a fault of its own |
| StreamCreationError | 0x103 | a second control, encoder or decoder stream; a push stream at a server |
| ClosedCriticalStream | 0x104 | the control, encoder or decoder stream ended |
| FrameUnexpected | 0x105 | a frame on a stream that does not carry it, or in an order it may not take |
| FrameError | 0x106 | a frame that could not be read: a length that does not add up |
| ExcessiveLoad | 0x107 | a frame over the size this endpoint advertised |
| IdError | 0x108 | a push id never promised, or used twice |
| SettingsError | 0x109 | a setting HTTP/2 defined, or the same identifier twice |
| MissingSettings | 0x10a | a control stream that opened with something other than SETTINGS |
| RequestRejected | 0x10b | a request this endpoint will not serve |
| RequestCancelled | 0x10c | a request the client abandoned |
| RequestIncomplete | 0x10d | a request stream that ended before the request did |
| MessageError | 0x10e | a malformed request: a repeated, absent or misplaced pseudo-header, a connection-specific field |
| ConnectError | 0x10f | the CONNECT method or its extended form misused |
| VersionFallback | 0x110 | a version this endpoint does not serve |
| QpackDecompressionFailed | 0x200 | a field section the table cannot resolve: a static index past the table, a dynamic entry that is not there |
| QpackEncoderStreamError | 0x201 | a table capacity over the one advertised, or an instruction that names no entry |
| QpackDecoderStreamError | 0x202 | an Insert Count Increment of 0 |
moon add moonbitstack/moonhttpInstall
Download zipHTTP formats for MoonBit with no I/O: HTTP/1 to HTTP/3, WebSocket, SSE and multipart.