Add HTTP/1.1 client and server #2
Merged
catbot
merged 4 commits from 2026-07-27 01:05:16 +00:00
claude/issue-1 into master 2026-07-27
docs(http1): document the HTTP/1.1 stack, and expose the client's timeout
README: HTTP/1.1 in the intro, feature list, module list, browser-build exclusions, dependencies and test list, plus a Components section covering both classes, the standalone codec, what is and is not implemented, the smuggling-shaped inputs that are rejected, and an explicit note that this path is plaintext and belongs behind a TLS terminator. ClientHTTP1::timeout was hard-coded and invisible; make it a public member alongside `limits`, mirroring the listener's timeouts. Also pipelining coverage in ShouldSendRecieveHTTP1: two requests written before either is answered, driven from a raw socket since ClientHTTP1 waits for each response. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(http1): close a finished connection instead of holding it until reap
A connection's socket was owned by the registry entry and only released when the next accept() reaped it, so a peer we had finished with — after a 408, a 400, or a `connection: close` — never saw EOF and sat waiting for a server that was done talking. On a server that goes quiet it also held every descriptor from the last burst indefinitely. The connection thread now closes its own socket the moment Serve() returns, under the registry lock so Stop()'s shutdown() can never name a descriptor that has already been released, and Stop() waits on a condition variable for the last thread rather than assuming the vector it moved out is quiescent. Adopt() is also fully guarded: it runs on ListenerTCP's accept loop, which has no handler, so anything escaping it would abort the process. Found by ShouldSurviveAbuseHTTP1, added here: 24 concurrent keep-alive clients, peers that vanish mid-request or send garbage, and a peer that stalls forever — the server must keep serving and still stop promptly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies and CI tooling still speak nothing but HTTP/1.1. This adds that path using the request/response types the HTTP/3 stack already uses, so a route handler or call site moves between the two protocols by changing the class name. - :HTTP1 — transport-free wire format. Serialisation with the framing headers owned by the serialiser, and an incremental parser that takes arbitrary socket chunks and yields one message at a time: keep-alive, pipelining, content-length and chunked bodies (with trailers), read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and Expect: 100-continue. Ambiguous framing is rejected rather than guessed at (content-length with transfer-encoding, disagreeing content-lengths, whitespace before a colon), and CR/LF in a value we are asked to serialise is refused. - ClientHTTP1 — persistent connection, redialling once when a pooled connection turns out to have been closed by the peer, which is the race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a response byte has arrived. - ListenerHTTP1 — one thread per connection (keep-alive connections are idle most of their life and would pin every ThreadPool thread), automatic Date, HEAD, 100-continue, handler-requested close, idle and request timeouts, and 400/404/500 responses. Routes fall back to the query-stripped path so `/thing?x=1` reaches the handler for `/thing`. No TLS: this is `http://` only. Encrypted traffic still goes over HTTP/3, or through a TLS-terminating proxy. Tests: codec unit tests including the malformed inputs above, a client/server round-trip, keep-alive and stale-connection recovery, a 10 MiB body both ways, and interop both directions against curl and python3's http.server (skipped when those are not installed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(tcp): resolve, connect, bind and send failures were silent or wrong
The HTTP/1.1 stack sits directly on these two classes and each of these bit it: - gethostbyname() returning null on an unresolvable host was dereferenced straight into a crash, and it is not thread safe; use getaddrinfo. - A failed socket()/connect() only printed to stderr and handed back an unusable ClientTCP, so the real error surfaced much later as an unrelated errno from send(). - send() was assumed to accept everything it was offered. It does not once a buffer outgrows the socket's send buffer, which silently truncated multi-megabyte bodies. Loop, and pass MSG_NOSIGNAL so a vanished peer raises EPIPE instead of killing the process. - ClientTCP's move constructor closed the socket it had just taken ownership of, and both it and the destructor tested `socketid != 1` where they meant `!= -1`. - ListenerTCP ignored bind()'s result, leaving a listener that accepted nothing with no explanation, and did not set SO_REUSEADDR, so a restart hit EADDRINUSE for the length of TIME_WAIT. accept() failing during Stop() is expected and no longer logged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>