moonhttp

Figures · nine plates

The wire,
drawn to scale.

A protocol is a shape before it is a rule. Each plate below is one of the shapes this library has to get right: a head, a chunk, a frame, a stream, and the arithmetic a cache does over a clock.

1 cell = 8 bits/RFC 9112 · 9113 · 9114 · 6455 · 7692 · 9111
FIG. 01

An HTTP/1.1 message

RFC 9112 §2
request, 66 octets of headCRLF ends every line
GET /notes/42 HTTP/1.1request-line: method SP target SP version
Host: example.comfield-line: name ":" value
If-None-Match: "v7"a field may be repeated, never folded
\r\n\r\nthe empty line that ends the head
{"seq":1}body, framed by Content-Length or Transfer-Encoding

What to notice

The head has no length field. It ends at the first empty line, which is why a reader scans for four octets rather than jumping to an offset — and why the octet count it returns matters: everything after it is body, or the next message on a kept-alive connection.

Fields are bytes. Deciding that "v7" is a quoted string is the next layer's problem, and a decision that can fail.

FIG. 02

Chunked body

RFC 9112 §7.1
Transfer-Encoding: chunkedsize in hex
1achunk size in hex — 26 octets
{"seq":1,"ok":true,"n":7}that many octets, then CRLF
0last chunk: a size of zero
trailer section, empty here

What to notice

The size is hex and counts the data, not the CRLF that follows it. A chunk extension may sit after the size behind a semicolon, and a reader that treats it as part of the number reads the wrong body.

The last chunk carries zero octets and is followed by the trailers — fields that belong to the message but arrive after its body.

FIG. 03

HTTP/2 frame header

RFC 9113 §4.1
nine octets, every framebig-endian
LENGTH24 bits
TYPE8
FLAGS8
R + STREAM ID1 + 31
012345678
Length counts the payload only, never the header.

What to notice

Every frame starts with the same nine octets, so a reader can skip a type it does not know by trusting the length — which is the point of putting the length first.

Flags mean different things per type: the same bit is END_STREAM on a DATA frame and END_HEADERS on a HEADERS one. The type and the flag are read together or not at all. The reserved bit is not spare: a receiver that sees it set treats the frame as a connection error.

FIG. 04

A variable-length integer

RFC 9000 §16
HTTP/3 and QUIC count in thisthe prefix is the width
PrefixWidthValueWhere it is used
001 octet6 bits · 0–63a small frame type or stream type
012 octets14 bits · 0–16 383most lengths
104 octets30 bits · 0–2³⁰−1a body that does not fit in 16 KiB
118 octets62 bits · 0–2⁶²−1the connection's stream count

What to notice

The first two bits say how many bits the value has, and the value is written in as few octets as its own size allows. The encoding is self-describing, so a reader never needs the field's type to parse it.

QUIC and HTTP/3 use the same integer, which is why it lives in moonvar rather than here.

FIG. 05

A WebSocket frame

RFC 6455 §5.2
client to server, two octets of payload1 cell = 1 bit
0x00
FIN1
RSV1
RSV2
RSV3
OPCODE4
0x01
MASK1
LENGTH7
MASKING KEY32 bits — the client's four octets
PAYLOAD2 octets, each XORed with the key
The same message on the wire: 81 82 01 02 03 04 69 6b — 81 is FIN and the text opcode, 82 is the mask bit and a length of two, and hi arrives as 69 6b.

What to notice

The mask is not encryption and is not meant to be: it exists so that a client cannot be tricked into sending octets a proxy would read as a request. That is also why a server must not mask, and why a client that does not mask is refused.

RSV1 belongs to the extension, which is the next plate.

FIG. 06

The stream permessage-deflate shares

RFC 7692 §7.2
one stream, three messagestail removed by the sender
message 1
f2 48 cd c9 c9 07 00
00 00 ff ff
message 2
2a 4e 4c 4a 2e 01 00
00 00 ff ff
message 3
a message that compressed to nothing
00 00 ff ff

One DEFLATE stream, stopped and resumed at each message boundary. The receiver puts the four octets back before decompressing, which is what makes the pieces one stream again.

What to notice

The window is not reset between messages unless the terms say server_no_context_takeover or client_no_context_takeover. A match in message 2 may reach into message 1, which is where most of the ratio comes from.

That is also why a message cannot be decompressed on its own: the stream, not the message, is the unit the compressor works in.

FIG. 07

How old is a stored response

RFC 9111 §4.2.3
four clock readings, one agedrag to change
0s
0s
1s
30s
60s
initial age
1s
current age
31s
left
29s
fresh — may be served

apparent = max(0, response_time − date)
corrected = age + (response_time − request_time)
current = max(apparent, corrected) + (now − response_time)

What to notice

The largest of the two ages wins: what the origin said, and what the trip added. That max is why a response dated in the future is not newborn — the delay still counts.

Freshness is age ≤ lifetime, not <. A response is stale once its age is greater than the lifetime, so the second it turns exactly is still fresh; move the sliders to the boundary and watch the verdict change.

FIG. 08

What a redirect does to the method

RFC 9110 §15.4
five statuses, two behavioursmethod and body together
StatusFromToBodyWhy
301POSTGETdroppedmoved permanently; the original said so
302POSTGETdroppedfound; what browsers have always done
303PUTGETdroppedsee other; the answer is elsewhere
307POSTPOSTkepttemporary; the method must not change
308PUTPUTkeptpermanent, and the same prohibition

What to notice

303 is the only one that rewrites for every method: it exists to turn a submission into a fetch. 307 and 308 exist for the opposite reason, and a client whose body cannot be replayed must refuse them rather than send an empty one.

Credentials are a separate decision from the method: Authorization and Cookie are dropped when the origin changes, compared by scheme, host and the effective port.

FIG. 09

An alternative service

RFC 7838 §3
Alt-Svc, as a server writes itone member of a list
h3=":443"; ma=3600protocol-id = alt-authority ; parameters
h3an ALPN name, percent-encoded when it has to be
":443"an empty host means the origin's own; the port is never implied
ma=3600seconds this stays fresh — 86400 when absent
persist=1optional: keep this across a change of network

What to notice

An alternative is authoritative for the origin, not for itself: a connection to it still presents the origin's name in TLS and its Host. That is what makes it a redirection rather than a different site.

clear as the whole value forgets every alternative the origin ever advertised. A member this reader cannot parse is dropped rather than failing the field: the others are still what the origin said.

Each plate is drawn from the section named in its header. The calculator runs RFC 9111 §4.2.3 with the same comparison the library uses.