moonhttp

MoonBit · HTTP family · no I/O

HTTP logic,
no sockets.

Requests and responses, WebSocket frames, cache lifetime, redirect targets: the parts of HTTP that are decided rather than sent, written as functions over bytes. Nothing here opens a connection.

$ moon add moonbitstack/moonhttp
request head66 octets
GET /notes/42 HTTP/1.1
Host: example.com
If-None-Match: "v7"

0224466
answer, if nothing changed42 octets
HTTP/1.1 304 Not Modified
ETag: "v7"

Three fields and an empty line, then the answer that carries no body. A cache decides which of the two it may send; the age of what it holds is what decides it.
19
packages
4
wire protocols
0
sockets
3
targets
wasm · js · native

What it holds

Six kinds of decision.

Each package is a piece of the specification with the octets taken seriously, and each one is usable on its own.

01

Framing

Where a frame starts, where it ends, and what the next one is. Chunked bodies, the nine-octet HTTP/2 header, HTTP/3 frames, WebSocket frames.

RFC 9112 · 9113 · 9114 · 6455
02

Messages

A request head, a status line, field lines, a body. Both ends of a connection, driven as octets in and events out.

http1 · http2 · http3
03

Compression

HPACK and QPACK with both tables, the Huffman code they share, and the DEFLATE a WebSocket needs: a window kept across messages.

RFC 7541 · 9204 · 7692
04

Caching

May this be stored, how long is it fresh, how old is it now, may it answer this request. Four questions, answered from fields and a clock.

RFC 9111
05

Decisions

Which redirect to follow, with which method, which credentials stop at the origin; which alternative service to try next.

RFC 9110 §15.4 · RFC 7838
06

Payloads

Forms in both encodings, byte ranges, conditional requests, cookies, event streams. The fields and bodies that arrive with a message.

RFC 6265 · 9110 §13, §14 · WHATWG

The wire, drawn

A frame is a byte layout.

Every length on this page is a number two ends had to agree about before the first message could exist.

Fig. 01 — WebSocket frame, RFC 6455 §5.2 1 cell = 1 bit
0x00
FIN1
RSV1
RSV2
RSV3
OPCODE4
0x01
MASK1
LENGTH7
MASKING KEY32 bits, client to server only
PAYLOADLENGTH octets
RSV1 is not spare. An extension may claim it, which is how permessage-deflate says a message is compressed; with no extension negotiated, a frame that sets it is a protocol error. The whole set, drawn to scale →

Reading a request

Bytes in, values out.

No handler signature to satisfy and no runtime to start: three calls, then a decision.

moonbit
// A request head, as it arrived.
let (request, used) = @http1.request(bytes[:])
request.target                     // "/notes/42"
request.field("if-none-match")     // Some(b'"v7"'), and used = 66

// Does the copy the client holds still match the body we have?
let tag = @conditional.hash(body[:], digest=@sha2.Hasher::new())
@conditional.Ask::read(request.fields)
  .evaluate(verb="GET", etag=tag)  // Go | Fresh | Failed

Packages

Nineteen, each with its section.

Small enough to read, separate enough to depend on one without the other eighteen.