# AGENTS.md Guidance for coding agents and contributors working on Crafter.Network itself. The [README](README.md) is the user-facing manual, and [AGENTS.consumer.md](AGENTS.consumer.md) is what agents working in a *consumer* of this library read: crafter-build copies it into the dependency-notes section of the consumer's `AGENTS.md` on every build. When any of the three disagrees with the code, the code wins; the interfaces under `interfaces/` are the contract. ## Project overview Crafter.Network is the Crafter engine's networking library: one C++ module, `Crafter.Network`, with fifteen partitions behind its primary interface (sixteen interface files), with TCP, URLs, a byte-stream seam and a libssl TLS transport, HTTP/1.1 (client, listener and a transport-free codec), QUIC over msquic, HTTP/3 (client, listener and a wire-format partition) and WebTransport on the HTTP/3 listener. One source tree builds three libraries: native Linux (everything), native with `--no-quic` (TCP, TLS and HTTP/1.1, no msquic), and the browser (`wasm32-*`: the client half of HTTP and WebTransport over `fetch()` and the `WebTransport` API, `*Async` only). The only crafter-build dependency is Crafter.Thread, whose `ThreadPool` runs every callback and `*Async` call; msquic is an `ExternalDependency` built with CMake, and OpenSSL 3 is a system package. | Path | What lives there | |---|---| | `interfaces/Crafter.Network.cppm` | The umbrella: `export import`s every partition, under `#ifndef CRAFTER_NETWORK_NO_QUIC` and `#ifndef CRAFTER_NETWORK_BROWSER` for the ones a build leaves out. | | `interfaces/Crafter.Network-.cppm` | One partition per file. The native-only ones wrap their whole body in `#ifndef CRAFTER_NETWORK_BROWSER`, so the browser build compiles them as empty modules. | | `implementations/Crafter.Network-.cpp` | The native implementation units, `Crafter.Network:_impl`. `HTTP`, `HTTP1`, `HTTP3` and `URL` have none: they are inline. | | `implementations/Crafter.Network-ClientHTTP-Browser.cpp`, `-ClientQUIC-Browser.cpp` | The browser implementations of those two partitions, over the JS bridge. | | `additional/network-env.js` | The JS bridge: `fetch()` and `WebTransport` behind the `env` imports the wasm declares, shipped next to a consumer's `.wasm` through `cfg.files`. | | `project.cpp` | The build description: one static library, `crafter-network`, in its three variants, the msquic external, the OpenSSL link flags and the test declarations. | | `tests//main.cpp` | One test program per directory, declared in `project.cpp`. | | `examples/SimpleClient/` | A wasm browser client and a native WebTransport echo server in one `project.cpp`, MIT licensed, meant to be copied. | | `AGENTS.template.md`, `AGENTS.consumer.md` | The source of this file, and the notes for consumers. Every build and test run writes `AGENTS.md` from the template, with the `AGENTS.consumer.md` of each dependency that ships one merged in, and only when the text changes (`test --list`, `lint`, `format`, `run` and `clean` leave it alone); `AGENTS.md` is gitignored, so edit the template. Crafter.Thread ships none today, so there is no notes section, and a fresh clone has no `AGENTS.md` until its first build, when it gets the template under crafter-build's header line. | | `build/`, `bin/` | Generated and gitignored, one directory per variant and per test; `build/test-logs/` keeps the output of failed tests. The example's `bin/` and `build/` match the same patterns. | Partitions and what they own: - `:ClientTCP`, `:ListenerTCP`: POSIX sockets, IPv4 and IPv6. `SocketClosedException`, `SocketTimeoutException`, `SocketErrorException` (a `std::system_error` with the `SocketPhase` that failed; `ResolverCategory()` for `getaddrinfo`'s codes), the receive semantics (what arrived, never a padded buffer) and the `onError` of the async receives. The listener hands its callback a `new ClientTCP` the callback owns. - `:Stream`: `ByteStream` (`ReadSome` and `Write` with a timeout per call, `Shutdown`, `Descriptor`, `Protocol`, `Secure`) and `PlainStream`, so one HTTP/1.1 implementation serves `http://` and `https://`. `SetNonBlocking`, `PollDescriptor`. - `:TLS`: `TLSContext`, `TLSStream`, both credential structs, `GetSelfSignedCertificatePem`. OpenSSL stays behind the `Impl` pointers; no OpenSSL header reaches an importer. - `:HTTP`: `HTTPRequest`, `HTTPResponse`, the `Create*` helpers, `PathWithoutQueryHTTP`. Shared by both HTTP versions and by every build. - `:URL`: `URL`, an absolute URL as RFC 3986 reads it (`Parse`), the resolution of a reference against it (`Resolve`, RFC 3986 §5.2, dot segments removed), the recomposition, and the pieces a request is built from. Inline, native only; what `ClientHTTP1::Fetch` follows a Location with. - `:HTTP1`: the RFC 9112 codec, inline: `SerializeRequest`, `SerializeResponse`, `SerializeContinue`, `MessageParser`, `MessageLimits`, `HTTP1ProtocolError`, and the framing it refuses to guess at. - `:ClientHTTP1`, `:ListenerHTTP1`: HTTP/1.1 over a `ByteStream&`. The listener is a `ListenerTCP` with a thread per accepted connection; the client keeps one pooled connection with a replay-once rule, and `ClientHTTP1::Fetch` is the URL fetch that follows redirects by RFC 9110 §15.4 with a client per hop (`FetchOptions`, `FetchResult`, `FetchStop`). - `:ClientQUIC`: `ClientQUIC` and `QUICStream` for both ends of a connection, `QUICSettings`, both QUIC credential structs, `QUICException`, `QUICClosedException`. The native unit holds the process-wide `MsQuicRuntime` (API table and registration, closed by a static destructor at exit), the stream registry with its refcounted handles, and the drain in `~ClientQUIC`. - `:ListenerQUIC`: the msquic listener, its four `Listen*` variants, the self-signed certificate (made by the `openssl` CLI and cached under `/tmp/crafter-network-quic-cert/`), `ComputeCertificateHashSHA256`, `GetSelfSignedCertificatePath`. - `:HTTP3`: QUIC varints, HTTP/3 frames, the QPACK static table, Huffman decoding, the control-stream preludes and the WebTransport stream prefixes. Native only: it throws for protocol errors. - `:ClientHTTP`, `:ListenerHTTP`: HTTP/3 on `:ClientQUIC` and `:ListenerQUIC`. The listener demuxes every bidirectional stream into an HTTP/3 request or a WebTransport data stream and accepts extended CONNECT when `wtRoutes` is non-empty. - `:WebTransport`: `WebTransportSession`, the per-session handle a WT handler gets. Bidirectional streams and session close today; datagrams and capsules are stubs. The variants are decided in `project.cpp`. Any `wasm32-*` target defines `CRAFTER_NETWORK_BROWSER`, compiles nine interfaces (the umbrella, `ClientTCP`, `ListenerTCP`, `ClientHTTP`, `ListenerHTTP`, `HTTP`, `ClientQUIC`, `ListenerQUIC`, `WebTransport`; all but the umbrella, `HTTP`, `ClientHTTP` and `ClientQUIC` empty) with the two `-Browser` units, ships `network-env.js`, and returns before msquic or OpenSSL come up. `--no-quic` defines `CRAFTER_NETWORK_NO_QUIC`, drops the six QUIC-side interfaces and their five units, and leaves msquic out, which is most of a fresh build. The flag is this project's own and is stripped from the args passed on to Crafter.Thread, so a consumer and this project load one Crafter.Thread rather than two with different args. ## Dev environment crafter-build is the only tool, from its Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; this machine has its `stable` and `beta` branches, and a build installed from a Crafter.Build checkout is branch `local`, chosen with `--branch=`). The host is Linux x86_64. A native build needs network access to `forgejo.catcrafts.net` for Crafter.Thread, which is unpinned and `git pull`ed by every verb that loads `project.cpp`, `test --list`, `lint` and `format` included (`--local` takes `../Crafter.Thread` instead, and is forwarded so Crafter.Thread resolves the same way), and to `github.com` once for msquic, pinned to v2.6.3 (commit `16ca4d8d`) with its submodules and built by CMake into `~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/external/msquic-/` (a native crafter-build uses `~/.cache/crafter.build/`): several minutes the first time, then shared by every project with the same spec. msquic is configured with `QUIC_TLS_LIB=openssl` against the OpenSSL the sandbox provides, 3.5 or newer (the Flatpak runtime has 3.5.9), as a shared `libmsquic.so` that the test binaries and consumers find through an absolute runpath into that cache (a directory named after msquic's spec hash, which `crafter-build clean` never touches). msquic links `libnuma` when the build host has it: a native crafter-build on Arch does, the Flatpak does not. What the suite needs beyond that: the `openssl` CLI (the QUIC self-signed certificate and its hash; the TLS partition makes its own certificate in-process), `curl` and `python3` for the interop tests, and outbound UDP/443 to `cloudflare-quic.com` for `ShouldSend`. The Flatpak sandbox has `openssl`, `curl`, `python3` and `wasmtime` and no `node`; its `/tmp` is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and discarded once none is, so the QUIC certificate cache under `/tmp` rarely survives between runs; and it sees the host filesystem but not the host's `PATH` (`/run/host/usr/bin` is the host's `/usr/bin`). The interop tests print `skipping the ... half` and pass when a tool is missing: a pass that tested less, so read the log. Build outputs go to `bin/crafter-network----/` (the PCMs, `std.pcm` and `libcrafter-network.a`; the wasm variant also carries `network-env.js`) and `build//`. The hash covers the variant, so the default build, `--no-quic` and the browser build keep separate directories, and `bin/` also holds one directory per test. The Flatpak's host triple is `x86_64-unknown-linux-gnu`; the README's `x86_64-pc-linux-gnu` default is a native crafter-build's, and `--target=` with either is the same build in another directory. `crafter-build clean` removes them all. ## Build, test and style commands ```bash crafter-build # native: TCP, TLS, HTTP/1.1, QUIC, HTTP/3, WebTransport crafter-build --no-quic # native without QUIC, HTTP/3, WebTransport and msquic crafter-build --local # Crafter.Thread from ../Crafter.Thread crafter-build --target=wasm32-wasip1 # the browser library crafter-build test # the suite, native: 30 tests crafter-build test 'Should*HTTP1' # a name glob crafter-build test --no-quic # the TCP, URL, TLS and HTTP/1.1 tests plus ShouldBuildWithoutQUIC: 19 crafter-build test --list crafter-build test --jobs=1 --timeout=120 crafter-build clean ``` A `--flag` crafter-build does not know is a project arg under any verb, so `--no-quic` and `--local` go anywhere before a `--`; under `test`, a bare word and everything after `--` are name globs, so `crafter-build test -- --no-quic` runs nothing. `project.cpp` registers no lint rules, so `crafter-build lint` and `format` exit 1 with "no rules"; style is checked by eye (see "Code style"). There is no CI. Before a change is done, `crafter-build test` and `crafter-build test --no-quic` pass, and `crafter-build --target=wasm32-wasip1` still builds when a `#ifndef CRAFTER_NETWORK_BROWSER` block, a browser unit or `network-env.js` was touched. Failed, crashed and timed-out tests write `build/test-logs/.log`; a later passing run does not delete them, so check the timestamp before trusting one. ## Testing instructions Tests are declared, not discovered: one `cfg.AddTest("").Dependencies({ &cfg })` line in `project.cpp`, inside the block for Linux targets (the browser build runs no tests), in the `if (quic)` branch when the test needs msquic, or in its `else` when it only exists to show msquic stayed out (`ShouldBuildWithoutQUIC`). The source is `tests//main.cpp`, linked against the local `libcrafter-network.a` with the variant's defines. A test is an `int main()`: 0 passes, nonzero fails, 77 skips (unused here; the interop tests skip a half by printing and passing). The runner gives each 60 s in its own process, in parallel. Conventions the tests follow: - Names are `Should`; the `SendRecieve*` and `Interop*` families end in `HTTP1` or `HTTPS1` on the HTTP/1.1 side and `HTTP` or `QUIC` on the QUIC side, and `ShouldSend`, `ShouldNotDropEarlyStreams`, `ShouldFallbackUnknownRoutes` and `ShouldSurviveConnectionChurn` carry no suffix. Open `main.cpp` with a comment on the behaviour under test and the failure it guards against; `tests/ShouldRecieveTCP/main.cpp` and `tests/ShouldSurviveConnectionChurn/main.cpp` show the shape, and a regression test names the bug. - A `Check(ok, what)` helper in an anonymous namespace counts failures and prints them (`PASS`/`FAIL` lines in the newer tests), and `main` returns nonzero at the end; the oldest QUIC tests return at the first failure. Do not stop at the first failure unless later steps are meaningless without it. - Each test owns fixed ports, distinct across the suite because the runner is parallel: 8090 to 8097 and 8110 to 8114 for HTTP/1.1 listeners, 8082 to 8086 and 8098 for HTTP/3, 9181, 9182, 9187 and 9188 for QUIC, 19200 to 19224 for the churn test; the two TCP tests bind ephemeral ports, and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. Pick an unused one for a new test, or bind an ephemeral port yourself (`LoopbackListener` in `ShouldConfigureTCP`). - `ThreadPool::Start()` first, for anything that enqueues: every `*Async` call, `ClientHTTP` (its constructor installs an `OnStream` drain that runs as pool tasks), `ListenerHTTP`, `ListenerQUIC`, and the `*Async`-dispatch `Listen*` variants of `ListenerTCP`; `ListenerHTTP1` and the sync TCP listeners need none, which is why ten tests never call it. `Start()` runs `hardware_concurrency() - 1` workers and `Start(1)` none at all (tasks then wait for a `JoinPool()` nobody calls), so don't use `Start(1)` to make callbacks deterministic. - A QUIC test ends in `std::_Exit(code)`, after `std::fflush(stdout)` since `_Exit` flushes nothing (`ShouldReportClosedQUICStream` does; the older ones skip the flush and can lose their last line in the log): msquic's two registrations are closed by static destructors that block until every connection is torn down, and the peers a listener accepted are heap-allocated for the callback's owner, so returning from `main` waits for the idle timeout. `ShouldSurviveConnectionChurn` is the exception on purpose: that wait is its assertion, so it deletes everything and returns, under a watchdog thread that calls `std::_Exit(1)` after 90 s, which the runner's 60 s timeout beats; a watchdog meant to fire goes under 60 s, as the 8 s one in `ShouldNotDropEarlyStreams` does. Any test that can hang gets one. - Credentials: `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }`; for TLS, `TLSServerCredentials{ .selfSigned = true }` against a client given `GetSelfSignedCertificatePem().certificate` as `caPem`, so verification is exercised rather than skipped. - A `ClientTCP` drives the wire where `ClientHTTP1` would hide the thing under test (pipelining in `ShouldSendRecieveHTTP1`, garbage and a stalled peer in `ShouldSurviveAbuseHTTP1`), and raw POSIX sockets (headers before the imports) where `ClientTCP` itself is under test (`ShouldRecieveTCP`, `ShouldConfigureTCP`, `ShouldBuildWithoutQUIC`). - The outside world: `curl`, `python3 -m http.server` and the `openssl` CLI are probed with `command -v`, and a missing one skips that half with a line saying so. `ShouldSend` is the one test that needs the network. A bug fix ships with a test that fails before it and passes after, in its own `test:` commit when it stands on its own; the `fix(quic)` commits in `git log` show the pairing. ## Code style Nothing is enforced by tooling; match the file you are in. - The first two lines of every source are exactly `//SPDX-License-Identifier: LGPL-3.0-only` and `//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®`, no space after `//`, then a blank line. The example is MIT in the same shape. (`Crafter.Network-ClientQUIC-Browser.cpp` has stray text after its copyright line; fix it when you touch the file.) - Four-space indentation in new code. `Crafter.Network-ClientTCP.cppm`, `-ListenerTCP.cppm` and parts of `-ClientTCP.cpp` and `-ListenerTCP.cpp` are tab-indented from the first version; match the block you are in and do not reindent files, it buries the real diff. `Crafter.Network.cppm` and `Crafter.Network-ListenerTCP.cppm` are CRLF and every other source LF; an editor that normalises line endings rewrites the whole file, the same burying. - `import std;` everywhere. C headers (``, ``, ``) go in the global module fragment after `module;`, and in an interface only under `#ifndef CRAFTER_NETWORK_BROWSER`, since the wasm sysroot has none of them. Tests `#include` POSIX headers before their imports the same way. - `PascalCase` for types and functions, `camelCase` for variables and members, `k`-prefixed constants in the codecs (`HTTP3::kFrameHeaders`). Exceptions are `Exception` or `ProtocolError`. `std::println` for output in new code; some tests still use `std::cout`. - Spellings that are API: `Recieve` in every receive method and test name, and the `ListenSyncSync` family. Consumers build against the default branch, so a rename breaks all of them at once: only on purpose, with the consumers updated in the same move, and never for a new name. - Comments say why: the RFC section, the failure mode, the race, the rejected alternative. The codebase carries long comments on purpose (the header of `:HTTP1`, the stream registry in `-ClientQUIC.cpp`, `MakeBidiHandler` in `-ListenerHTTP.cpp`); do not strip them, and write them for new non-obvious code. Commit bodies follow the same rule. - Errors are exceptions on the synchronous paths and never leave a pool task: an `*Async` call reports through its callback (`onError(std::exception_ptr)` on TCP, `bool sent` and the empty chunk on QUIC streams, an error string on the HTTP clients), because an exception escaping a `ThreadPool` task ends the program and the browser bridge has no exception channel from JS. - Timeouts are per call on `ByteStream`, members on the HTTP/1.1 endpoints, `QUICSettings` on QUIC, and `std::chrono::milliseconds` everywhere. A new blocking path gets a bound. ## Design rules Decided deliberately; change them only with the maintainer's say-so. 1. **One source, three builds.** Native, `--no-quic` and the browser are the same files, with `#ifndef CRAFTER_NETWORK_BROWSER` around whole partitions, `#ifndef CRAFTER_NETWORK_NO_QUIC` in the umbrella, and two `-Browser` implementation units. No per-platform directory and no host-side `#ifdef` beyond those two. 2. **The browser is a client, and async.** No listener, no raw TCP and no `*Sync` method there; the JS bridge drives `fetch()` and `WebTransport` and calls back through wasm exports. An API that cannot be made async does not join the shared surface. 3. **One request and response type, one route map, one dispatch order** across HTTP/1.1 and HTTP/3: exact path, query-stripped path, `fallback`, 404. A handler moves between the two by changing a class name, and `ShouldFallbackUnknownRoutes` replays one table over both to keep it so. 4. **`fallback` is a hook, not a pattern syntax.** Consumers bring their router; the library never grows a second route table that can disagree with it. 5. **Ambiguous HTTP/1.1 framing is rejected, not guessed** (RFC 9112 §11.2): conflicting lengths, a length with chunked, whitespace before a colon, CR or LF in a value to send. Compatibility never buys a parse that could smuggle. 6. **`https://` is a constructor argument, not a class.** The HTTP/1.1 code holds a `ByteStream&` and never learns whether it is a `PlainStream` or a `TLSStream`; `:TLS` is protocol-agnostic and keeps OpenSSL out of the interface. 7. **One OpenSSL in the process.** msquic builds against the system OpenSSL (3.5 or newer), not its bundled quictls, whose reading of the system `openssl.cnf` broke inside the Flatpak; `:TLS` links the same `libssl`. Where that OpenSSL is too old the answer is `--no-quic`, not a second TLS stack. 8. **msquic is pinned, Crafter.Thread is not.** Externals from outside Catcrafts get a `commit`; sibling Catcrafts repositories track their default branch on purpose. 9. **Receives return what arrived.** No padded buffers, no truncation, no exception for a signal, and a receive of zero bytes returns at once. The async variants deliver through callbacks and never throw out of the pool. 10. **The HTTP/1.1 listener runs a thread per connection**, not pool tasks: keep-alive connections are idle most of their life and would pin the pool. 11. **msquic handles are refcounted, and streams drain before their connection closes.** `QUICStream::Stop` initiates, the asynchronous `SHUTDOWN_COMPLETE` closes, `~ClientQUIC` waits (bounded) for the registry to empty. Do not short-circuit it: closing a connection with streams open is an msquic bugcheck, and a leaked handle hangs `RegistrationClose` at exit. 12. **This project's flags stay here.** `--no-quic` is stripped before the args go to Crafter.Thread, so every project in a consumer's graph loads Crafter.Thread with the same args. ## Commit and PR guidelines - Branch off `master` (`claude/issue-` for issue work, `claude/` or `fix/` otherwise) and open the PR against `master` on the project's Forgejo at `forgejo.catcrafts.net/Catcrafts/Crafter.Network`. `origin` is the HTTPS URL for fetch and push on this clone; Crafter.Build's notes say the server refuses HTTPS pushes, so set an SSH push URL first (`git remote set-url --push origin forgejo@forgejo.catcrafts.net:Catcrafts/Crafter.Network.git`), as the Crafter.Build, Math, Asset and Graphics clones have. - Subject: `type(scope): imperative summary`, with `feat`, `fix`, `docs` or `test` and the scopes in use: `tcp`, `tls`, `https`, `http1`, `http`, `quic`, `listener`. Body: what was wrong, why, and why the fix has this shape, wrapped at about 75 columns, with the numbers when a race was measured (`git log` for `fix(quic)` shows the voice), and a `Breaking:` paragraph when a signature changes. Add a `Co-Authored-By:` trailer when an agent wrote it. - Separate commits for the fix, its test and the README change when each stands on its own. Update the README in the same PR when user-visible behaviour changes, and `AGENTS.consumer.md` when a signature, a default or a rule consumers lean on changes; consumers pick the notes up on their next build. - `crafter-build test` and `crafter-build test --no-quic` green before pushing, and the wasm build when the browser surface was touched. There is no CI. - Never commit `bin/`, `build/`, `AGENTS.md` or the example's build outputs. `examples/SimpleClient/cert-hash.txt` is tracked because `cfg.files` needs it to exist; the native server overwrites it at runtime, and a changed hash in a diff is noise. ## Gotchas - **`ThreadPool::Enqueue` before `ThreadPool::Start()` is a null dereference**, and most of the library enqueues: every `*Async`, every QUIC `OnStream` and `OnDatagram` delivery, `ClientHTTP` and `ListenerHTTP` entirely. The crash has no message; in a test it shows as 💥. - **Two self-signed certificates.** `QUICServerCredentials::selfSigned` is made by the `openssl` CLI and cached at `/tmp/crafter-network-quic-cert/`, reused across restarts while valid so a browser's pinned hash survives a restart, except inside the Flatpak, whose `/tmp` goes away with its last instance (see "Dev environment"); `TLSServerCredentials::selfSigned` is made in-process by `:TLS` and is new every start. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second. - **`ComputeCertificateHashSHA256` and the QUIC certificate shell out to `openssl`** (`std::system`, `popen`) and throw `QUICException` without the CLI. `:TLS` could do both in-process; nobody has moved them yet. - **msquic's registration is closed by a static destructor that waits for every handle.** A test that returns from `main` with an accepted peer alive waits two minutes, the idle timeout; the tests `_Exit`. The exception, `ShouldSurviveConnectionChurn`, exists because a leaked stream handle once hung that close forever (`fix(quic)` in `git log`, with the `SHUTDOWN_ON_FAIL` story). - **Callbacks the interfaces say run "on the msquic worker" run as pool tasks**: `ClientQUIC::OnStream` and `OnDatagram` both enqueue. What arrives before a handler is set is queued: streams are delivered on `OnStream`; datagrams go to `RecieveDatagramSync`'s queue and never to a later `OnDatagram`. - **`ListenerQUIC::ListenAsync*` run the accept loop on a `std::thread`**, not the pool as their comments say, and `ListenerHTTP::Listen` is `ListenSyncAsync` underneath, so a `ListenerHTTP`'s per-connection setup is a pool task. A saturated pool delays a new connection's SETTINGS; the early streams are buffered meanwhile (`ShouldNotDropEarlyStreams`). - **A `ClientQUIC` against an HTTP/3 server must drain the server's unidirectional streams** (control, QPACK encoder and decoder) with an `OnStream` reader, or they back up and msquic may abort the connection. `ClientHTTP` does it; a hand-rolled WebTransport client (`ShouldEchoWebTransport`) does it itself. - **`QUICSettings::peerUnidiStreamCount` is the first thing to raise** when a peer stalls opening streams; it was a hardcoded 16 until `ShouldHonourQUICSettings`. - **The comments about `-fno-exceptions` in `project.cpp`, the umbrella and the browser unit are history.** crafter-build 4.0.0 compiles wasm with exceptions on and refuses `-fno-exceptions`; `:HTTP3` stays out of the browser build and the browser units still report through callbacks because the JS side has no exception channel, not because `throw` would not compile. - **The header comment of `Crafter.Network-HTTP3.cppm` is stale**: the partition is exported from the umbrella natively and has a Huffman decoder. The README is right. - **More comments the code outgrew**: `-ListenerQUIC.cpp` says the certificate pair lives in a `mkdtemp`'d directory for the life of the process (it is the fixed `/tmp/crafter-network-quic-cert/`, reused) and is valid 13 days (`-days 10`); `ClientQUIC.cppm` calls `selfSigned` an in-memory certificate (it is on disk, made by the CLI); `ListenerHTTP.cppm` says `Listen()` dispatches on the calling thread (it is `ListenSyncAsync`); the link-flag comment in `project.cpp` still has msquic bundling quictls (it links the system `libssl`). Fix a comment when you touch its code. - **`WebTransportSession::OpenStream(true)` does not throw** as its comment says; it writes the unidirectional prefix. Peer-initiated unidirectional WebTransport streams are drained by the listener and never reach a session. - **`ListenerTCP::concurrentClientLimit` is stored and never read**; only `totalClientLimit` ends the loop. - **`ListenerTCP(port, ...)` listens on every IPv4 address**; given an address it binds that one, IPv4 or IPv6, and port 0 asks the kernel for one. `ClientTCP` resolves for both families and tries every address; QUIC listens and connects on either family (`QUIC_ADDRESS_FAMILY_UNSPEC`). - **The Flatpak's host triple is `x86_64-unknown-linux-gnu`**, and the tests register on any Linux triple since the TCP-only commit; before it they never ran inside the Flatpak. - **A port in use from a crashed run** is `Could not bind port : Address already in use` on the HTTP/1.1 side and a `ListenerStart failed` `QUICException` on the QUIC side; find the stray process rather than changing the port. - **A warning is printed only when its unit actually recompiles**; a build with nothing to do is silent and says nothing about warnings.