The smallest HTTP the caller needs
My door speaks to exactly one client, and I know which one: Oriel's Python, using the standard library's http.client. Oriel's _MuxHTTP is an http.client.HTTPConnection whose connect() is overridden to open a usbmuxd socket to the phone by its UDID instead of dialing an address. Everything above the socket is stock Python. So the door does not need to be a web server. It needs to be the smallest HTTP that this client sends.
The doc comment on HTTPRequest writes that contract down in four parts: one request per connection, a Content-Length body and never a chunked one, Connection: close on every answer, and anything else "refused with the reason, never guessed at". The whole implementation is HTTPWire.swift, 189 lines with no dependency beyond Foundation.
A parser that only says three things
HTTPParser.parse (first block) is a pure function of the bytes a connection has delivered so far, and it answers one of three things. .incomplete means read more. .complete carries the request. .invalid carries a WireError with a sentence. Nothing is kept between calls: the door appends each new chunk to its buffer and asks again from the start. That sounds wasteful until you look at what the second call does. The head ends within the first few hundred bytes, the search for the blank line stops there, and the body is only counted until enough of it has arrived.
Read the guards in order, because each one names a failure in its own words:
- A head with no blank line yet is incomplete, unless it is already over 16 KB, which is
tooLarge. - A head that is not UTF-8 is refused.
- The request line must split into three parts and the third must start with
HTTP/1.. - Every header line needs a colon. Names are lowercased, values trimmed, and a repeated name keeps its last value.
- A
Transfer-Encodingother thanidentityis refused: "chunked bodies are not spoken here; send Content-Length". Content-Lengthmust be a non-negative integer and at most 32 MB. The constant's comment gives the size its reason: a minute and a half of speech is about 1.5 MB as MP3, "so 32 MB is room, not a target".
Only then is the target split into a path and a query, percent-decoded, so GET /status?x=1&y=a%20b becomes the path /status and the query ["x": "1", "y": "a b"]. That exact case is a test, readsAGetWithQuery.
An answer that always closes
HTTPResponse.serialized() (second block) writes the bytes, and it sets two headers itself, whatever the route asked for: Content-Length from the body and Connection: close. Header names go out sorted, so the same answer is always the same bytes, which is what lets answersCloseWithLength compare a whole response as a string. Any CR or LF inside a header value becomes a space. That matters because some values come from the world: a photo's facts ride the X-Cordelia-Photo header as JSON, and aHeaderValueCannotSplitTheHead proves that a value carrying \r\nInjected: yes cannot add a header of its own.
Refusals share one shape too. HTTPResponse.refuse(status, reason) is JSON with ok false and the reason under error, written with sorted keys. The next lesson is about those reasons.
The life of one connection
The app's BodyServer owns the socket, and its receive (third block) is the loop. A new connection is first checked against the home network (the next track's subject), then started on the server's own dispatch queue, never the main actor. Each read takes up to 1 MiB and hands the whole buffer to the parser. An incomplete request at the end of the stream is dropped. An invalid one gets 413 when it was too large and 400 otherwise, with the parser's sentence as the reason. A complete one is routed in a Task, and the answer is sent and the connection cancelled once Network reports the bytes processed.
A timer guards the arrival. A connection that has not delivered a whole request within 30 s (idleSeconds) is cancelled. The word that matters there is arrival, and it was learned the hard way, as the war story below tells. A small Arrival box carries one flag, whole, set the moment the parser says complete, and the idle cut checks it first.
Why not keep the connection open
Keep-alive saves a handshake per request, and here the handshake is a usbmuxd connect on a cable or a tunnel connect on the home Wi-Fi, both cheap at Oriel's pace (a status probe every 10 s). What it would cost is state: which request a byte belongs to, when an idle connection may close, what a half-read body means for the next request. With one request per connection, a failure belongs to exactly one request, a long answer (a line being played, Oriel's long poll of up to 25 s) holds only its own connection, and every other request opens another. The door even uses its own wire to test itself: every 20 s it sends GET /hello to its own loopback port, reads the first bytes of the answer for HTTP/1.1 200, and reopens the listener after two misses in a row.