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:
parent
b758419007
commit
337ce32eca
12 changed files with 2175 additions and 4 deletions
18
project.cpp
18
project.cpp
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in a new issue