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>
This commit is contained in:
catbot 2026-07-27 00:45:09 +00:00
commit 337ce32eca
12 changed files with 2175 additions and 4 deletions

View file

@ -7,13 +7,16 @@ namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
constexpr std::array<std::string_view, 10> networkInterfaces = {
constexpr std::array<std::string_view, 13> networkInterfaces = {
"interfaces/Crafter.Network",
"interfaces/Crafter.Network-ClientTCP",
"interfaces/Crafter.Network-ListenerTCP",
"interfaces/Crafter.Network-ClientHTTP",
"interfaces/Crafter.Network-ListenerHTTP",
"interfaces/Crafter.Network-HTTP",
"interfaces/Crafter.Network-HTTP1",
"interfaces/Crafter.Network-ClientHTTP1",
"interfaces/Crafter.Network-ListenerHTTP1",
"interfaces/Crafter.Network-HTTP3",
"interfaces/Crafter.Network-ClientQUIC",
"interfaces/Crafter.Network-ListenerQUIC",
@ -69,11 +72,13 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
return cfg;
}
constexpr std::array<std::string_view, 7> networkImplementations = {
constexpr std::array<std::string_view, 9> networkImplementations = {
"implementations/Crafter.Network-ClientTCP",
"implementations/Crafter.Network-ListenerTCP",
"implementations/Crafter.Network-ClientHTTP",
"implementations/Crafter.Network-ListenerHTTP",
"implementations/Crafter.Network-ClientHTTP1",
"implementations/Crafter.Network-ListenerHTTP1",
"implementations/Crafter.Network-ClientQUIC",
"implementations/Crafter.Network-ListenerQUIC",
"implementations/Crafter.Network-WebTransport",
@ -103,9 +108,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
// linker at the actual output location.
msquic.libDirs = { "bin/Release" };
msquic.libs = { "msquic" };
std::array<fs::path, 10> ifaces;
std::array<fs::path, 13> ifaces;
std::ranges::copy(networkInterfaces, ifaces.begin());
std::array<fs::path, 7> impls;
std::array<fs::path, 9> impls;
std::ranges::copy(networkImplementations, impls.begin());
cfg.GetInterfacesAndImplementations(ifaces, impls);
@ -114,8 +119,13 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
// crafter-network static lib via .Dependencies({ &cfg }).
if (cfg.target == "x86_64-pc-linux-gnu") {
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP1").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP").Dependencies({ &cfg });
cfg.AddTest("ShouldSendRecieveLargeHTTP").Dependencies({ &cfg });