2026-07-22 17:55:45 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
2026-05-06 01:06:05 +02:00
|
|
|
import std;
|
|
|
|
|
import Crafter.Build;
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
constexpr std::array<std::string_view, 13> networkInterfaces = {
|
2026-05-06 01:06:05 +02:00
|
|
|
"interfaces/Crafter.Network",
|
|
|
|
|
"interfaces/Crafter.Network-ClientTCP",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerTCP",
|
|
|
|
|
"interfaces/Crafter.Network-ClientHTTP",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerHTTP",
|
|
|
|
|
"interfaces/Crafter.Network-HTTP",
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
"interfaces/Crafter.Network-HTTP1",
|
|
|
|
|
"interfaces/Crafter.Network-ClientHTTP1",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerHTTP1",
|
2026-05-07 00:06:44 +02:00
|
|
|
"interfaces/Crafter.Network-HTTP3",
|
2026-05-06 04:06:17 +02:00
|
|
|
"interfaces/Crafter.Network-ClientQUIC",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerQUIC",
|
2026-05-19 02:53:50 +02:00
|
|
|
"interfaces/Crafter.Network-WebTransport",
|
2026-05-06 01:06:05 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
|
|
|
|
Configuration* thread = GitProject({
|
|
|
|
|
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Thread.git" },
|
|
|
|
|
.args = depArgs,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Configuration cfg;
|
|
|
|
|
cfg.path = "./";
|
2026-05-06 04:06:17 +02:00
|
|
|
cfg.name = "crafter-network";
|
|
|
|
|
cfg.outputName = "crafter-network";
|
2026-05-06 01:06:05 +02:00
|
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
|
|
|
ApplyStandardArgs(cfg, args);
|
|
|
|
|
cfg.dependencies = { thread };
|
|
|
|
|
|
2026-05-19 02:53:50 +02:00
|
|
|
// Browser path: any wasm32-* target gets the browser network stack
|
|
|
|
|
// (fetch + WebTransport via JS glue). msquic and the POSIX socket
|
|
|
|
|
// backends are skipped; the listener / TCP partitions stub to empty
|
|
|
|
|
// modules via #ifdef CRAFTER_NETWORK_BROWSER in their interface files.
|
|
|
|
|
// HTTP3 (varint / frame / QPACK codec) is dropped entirely — it threw
|
|
|
|
|
// exceptions for protocol errors, which the wasm build's -fno-exceptions
|
|
|
|
|
// forbids, and the browser's fetch() handles HTTP-layer framing itself.
|
|
|
|
|
bool browser = cfg.target.find("wasm") != std::string::npos;
|
|
|
|
|
if (browser) {
|
|
|
|
|
cfg.defines.push_back({"CRAFTER_NETWORK_BROWSER", ""});
|
|
|
|
|
|
|
|
|
|
std::array<fs::path, 9> browserIfaces = {
|
|
|
|
|
"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-ClientQUIC",
|
|
|
|
|
"interfaces/Crafter.Network-ListenerQUIC",
|
|
|
|
|
"interfaces/Crafter.Network-WebTransport",
|
|
|
|
|
};
|
|
|
|
|
std::array<fs::path, 2> browserImpls = {
|
|
|
|
|
"implementations/Crafter.Network-ClientHTTP-Browser",
|
|
|
|
|
"implementations/Crafter.Network-ClientQUIC-Browser",
|
|
|
|
|
};
|
|
|
|
|
cfg.GetInterfacesAndImplementations(browserIfaces, browserImpls);
|
|
|
|
|
|
|
|
|
|
// JS glue shipped alongside the .wasm. The consuming executable's
|
|
|
|
|
// wasi-browser runtime merges this into the env import object
|
|
|
|
|
// before instantiation (mirrors Crafter.Graphics/dom-env.js).
|
|
|
|
|
cfg.files.emplace_back(fs::path("additional/network-env.js"));
|
|
|
|
|
return cfg;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
constexpr std::array<std::string_view, 9> networkImplementations = {
|
2026-05-19 02:53:50 +02:00
|
|
|
"implementations/Crafter.Network-ClientTCP",
|
|
|
|
|
"implementations/Crafter.Network-ListenerTCP",
|
|
|
|
|
"implementations/Crafter.Network-ClientHTTP",
|
|
|
|
|
"implementations/Crafter.Network-ListenerHTTP",
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
"implementations/Crafter.Network-ClientHTTP1",
|
|
|
|
|
"implementations/Crafter.Network-ListenerHTTP1",
|
2026-05-19 02:53:50 +02:00
|
|
|
"implementations/Crafter.Network-ClientQUIC",
|
|
|
|
|
"implementations/Crafter.Network-ListenerQUIC",
|
|
|
|
|
"implementations/Crafter.Network-WebTransport",
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-06 04:06:17 +02:00
|
|
|
// msquic — provides the QUIC transport used by ClientQUIC / ListenerQUIC.
|
|
|
|
|
// Cloned + built via CMake into the per-project external cache; no system
|
|
|
|
|
// package required. Submodules (quictls / clog / etc.) come via the
|
|
|
|
|
// recursive clone Crafter.Build performs. We disable msquic's own tests,
|
|
|
|
|
// tools and perf binaries since we only need the library.
|
|
|
|
|
ExternalDependency& msquic = cfg.externalDependencies.emplace_back();
|
|
|
|
|
msquic.name = "msquic";
|
|
|
|
|
msquic.source.url = "https://github.com/microsoft/msquic.git";
|
|
|
|
|
msquic.source.branch = "main";
|
|
|
|
|
msquic.builder = ExternalBuilder::CMake;
|
|
|
|
|
msquic.options = {
|
|
|
|
|
"-DQUIC_TLS_LIB=quictls",
|
|
|
|
|
"-DQUIC_BUILD_TEST=OFF",
|
|
|
|
|
"-DQUIC_BUILD_TOOLS=OFF",
|
|
|
|
|
"-DQUIC_BUILD_PERF=OFF",
|
|
|
|
|
"-DQUIC_BUILD_SHARED=ON",
|
2026-05-06 01:06:05 +02:00
|
|
|
};
|
2026-05-06 04:06:17 +02:00
|
|
|
msquic.includeDirs = { "src/inc" };
|
|
|
|
|
// msquic's CMakeLists overrides CMAKE_LIBRARY_OUTPUT_DIRECTORY with
|
|
|
|
|
// QUIC_OUTPUT_DIR (defaults to bin/$<CONFIG>), so libmsquic.so lands in
|
|
|
|
|
// a subdir of the cmake build dir rather than at its root. Point the
|
|
|
|
|
// linker at the actual output location.
|
|
|
|
|
msquic.libDirs = { "bin/Release" };
|
|
|
|
|
msquic.libs = { "msquic" };
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
std::array<fs::path, 13> ifaces;
|
2026-05-06 04:06:17 +02:00
|
|
|
std::ranges::copy(networkInterfaces, ifaces.begin());
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
std::array<fs::path, 9> impls;
|
2026-05-06 04:06:17 +02:00
|
|
|
std::ranges::copy(networkImplementations, impls.begin());
|
|
|
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
2026-05-06 01:06:05 +02:00
|
|
|
|
2026-05-28 16:48:55 +02:00
|
|
|
// Linux-only: msquic + POSIX socket backends. The browser path above
|
|
|
|
|
// returns early, so wasm builds skip these. Each test links the local
|
|
|
|
|
// crafter-network static lib via .Dependencies({ &cfg }).
|
|
|
|
|
if (cfg.target == "x86_64-pc-linux-gnu") {
|
|
|
|
|
cfg.AddTest("ShouldEchoWebTransport").Dependencies({ &cfg });
|
test(http): replay one fallback route table over both listeners
The point of the feature is that a URL means the same thing over either
protocol, so the test states it that way: one route map plus one
fallback, registered with ListenerHTTP1 and ListenerHTTP, asked the same
eight questions, asserting identical answers.
Covers exact routes beating the fallback, query strings still routing to
the bare path, the fallback seeing the full target including the query,
the fallback choosing its own status (404 and 303), a throwing fallback
becoming a 500, and an unset fallback still producing the listener's own
synthetic 404.
Verified against a broken build both ways: dropping the fallback lookup
fails 9 checks, dropping the query-strip fails 2.
2026-07-28 19:42:16 +00:00
|
|
|
cfg.AddTest("ShouldFallbackUnknownRoutes").Dependencies({ &cfg });
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldInteropCurlHTTP1").Dependencies({ &cfg });
|
2026-05-29 16:56:07 +02:00
|
|
|
cfg.AddTest("ShouldNotDropEarlyStreams").Dependencies({ &cfg });
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldParseHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
cfg.AddTest("ShouldSend").Dependencies({ &cfg });
|
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>
2026-07-27 00:45:09 +00:00
|
|
|
cfg.AddTest("ShouldSendRecieveHTTP1").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP1").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveLargeHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
cfg.AddTest("ShouldSendRecieveHTTP").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveKeepaliveHTTP").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveLargeHTTP").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveQUICDatagram").Dependencies({ &cfg });
|
|
|
|
|
cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg });
|
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>
2026-07-27 00:56:53 +00:00
|
|
|
cfg.AddTest("ShouldSurviveAbuseHTTP1").Dependencies({ &cfg });
|
2026-05-28 16:48:55 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:06:05 +02:00
|
|
|
return cfg;
|
|
|
|
|
}
|