Add HTTP/1.1 client and server #2
No reviewers
Labels
No labels
claude:blocked
claude:done
claude:failed
claude:in-progress
claude:ready
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Catcrafts/Crafter.Network!2
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/issue-1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds an HTTP/1.1 client and server so the library can talk to the large part of the world that is not ready for HTTP/3 — old proxies, load balancers, CI tooling,
curlscripts.Resolves #1
What this adds
ClientHTTP1andListenerHTTP1use the sameHTTPRequest/HTTPResponsetypes and the same route-map shape as the HTTP/3 stack, so a handler or call site moves between the two protocols by changing the class name:Three new module partitions, all native-only (a browser page cannot open a raw TCP socket, and
fetch()behindClientHTTPalready negotiates whatever version the server offers)::HTTP1— the wire format on its own, with no transport dependency. Serialisation with the framing headers owned by the serialiser rather than the caller, plus an incrementalMessageParserthat takes arbitrary socket chunks and yields one message at a time.:ClientHTTP1— persistent connection, redialling once when a pooled connection turns out to have been closed by the peer. Nothing is replayed after a response byte has arrived, and a freshly dialled connection is never retried on, so a genuinely broken server surfaces as an exception rather than a retry loop.:ListenerHTTP1— one thread per connection. Deliberately not a ThreadPool task: keep-alive connections are idle most of their life and would otherwise pin every pool thread.Covered: keep-alive, pipelining,
content-lengthandchunkedbodies with trailers,Expect: 100-continue, HEAD, absolute-form targets, obs-fold, interim 1xx responses, read-to-EOF responses, HTTP/1.0 peers, automaticDate, handler-requested close, per-connection timeouts, and 400/404/500. Routing falls back to the query-stripped path, so/thing?x=1reaches the handler for/thingwhile the handler still sees the full target.Ambiguous framing is rejected rather than guessed at, because guessing is how request smuggling happens (RFC 9112 §11.2):
Content-LengthwithTransfer-Encoding, disagreeing duplicateContent-Lengthvalues, and whitespace before a header's colon are all 400s. CR/LF in a header value we are asked to send throws instead of splitting the message.No TLS. This is
http://only, and the README says so plainly: for encrypted traffic use HTTP/3 over QUIC, or terminate TLS in a proxy in front of it.Fixes to the TCP layer underneath
The HTTP/1.1 stack sits on
ClientTCP/ListenerTCPand each of these bit it:gethostbyname()returning null on an unresolvable host was dereferenced straight into a crash, and it is not thread safe →getaddrinfo.socket()/connect()only printed to stderr and handed back an unusableClientTCP, so the real error surfaced much later as an unrelated errno.send()was assumed to accept everything offered. It does not once a buffer outgrows the socket's send buffer, which silently truncated multi-megabyte bodies. Now loops, and passesMSG_NOSIGNALso a vanished peer raisesEPIPEinstead of killing the process.ClientTCP's move constructor closed the socket it had just taken ownership of, and it and the destructor both testedsocketid != 1where they meant!= -1.ListenerTCPignoredbind()'s result — leaving a listener that accepted nothing with no explanation — and did not setSO_REUSEADDR, so a restart hitEADDRINUSEfor the length of TIME_WAIT.One bug in the new code was found by its own test and is fixed in
ea1310f: a finished connection's socket was held until the next accept reaped it, so a peer we had finished with never saw EOF and sat waiting for a server that was done talking.Tests
Six new tests, all passing:
ShouldParseHTTP1ShouldSendRecieveHTTP1ShouldSendRecieveKeepaliveHTTP1ShouldSendRecieveLargeHTTP1ShouldInteropCurlHTTP1curlagainst the listener (keep-alive reuse, chunked upload,Expect: 100-continueverified from curl's own trace, HEAD, status codes), and the client against python3'shttp.server, which answers HTTP/1.0 withConnection: closeShouldSurviveAbuseHTTP1About that one:
ShouldSendis the pre-existing live-interop test againstcloudflare-quic.com:443. It fails here because this sandbox's egress allowlist blocks outbound UDP/443 —curl --http3-only https://cloudflare-quic.com/cannot connect either, and the test times out identically onmasterbefore any change in this branch. The README already documents that this test requires outbound UDP/443. Nothing in this PR touches the QUIC or HTTP/3 path.Note for reviewers
While working on this I hit — and filed upstream as Crafter.Build#26 — a build-system bug: when a library's module interface changes,
crafter-build testrelinks the test but does not recompile itsmain.cpp, producing ABI-mismatch crashes that look like bugs in the code under test. If you see unexplained SIGSEGVs after editing a.cppm,rm -rf build binfirst. The numbers above are from a clean build.