Crafter.Network/AGENTS.template.md

139 lines
30 KiB
Markdown
Raw Normal View History

# 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-<Partition>.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-<Partition>.cpp` | The native implementation units, `Crafter.Network:<Partition>_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/<Name>/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, a `SendRequest` per send carrying its own outcome, the per-stream delivery of `OnRecieve`'s and `SendAsync`'s callbacks (whichever thread finds nobody delivering runs what is ready; the rest queue and return), the `CloseNotice` behind `OnClosed`, and the drain in `~ClientQUIC` (the streams already stopped, then every stream, then the connection's own `SHUTDOWN_COMPLETE`).
- `:ListenerQUIC`: the msquic listener, its four `Listen*` variants, the self-signed certificate's on-disk cache (per user under `$XDG_CACHE_HOME/crafter-network/quic-cert/`, behind a lock; the OpenSSL work is module-internal functions of `:TLS`), `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-<hash>/` (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: `curl` and `python3` for the interop tests, the `openssl` CLI for the mutual-TLS half of `ShouldRequireClientCertificateHTTPS1` (both development certificates are made in-process), 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; its `XDG_CACHE_HOME` is `~/.var/app/net.catcrafts.CrafterBuild/cache`, so the QUIC tests' certificate lives in `crafter-network/quic-cert/` there and survives between runs (delete the directory to start cold; `ShouldShareSelfSignedCertificateQUIC` points `XDG_CACHE_HOME` at a scratch directory of its own); 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-<target>-<march>-<mtune>-<hash>/` (the PCMs, `std.pcm` and `libcrafter-network.a`; the wasm variant also carries `network-env.js`) and `build/<the same>/`. 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: 39 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/<Name>.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("<Name>").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/<Name>/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<Verb><What>`; 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, as do the QUIC tests written since `ListenerQUIC::Port()` (port 0, read back), and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. A new test binds port 0 and reads `Port()` (`ListenerTCP` and `ListenerQUIC` both have it); a fixed port is for when it cannot.
- `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` and `ShouldNotifyClosedConnectionQUIC` are the exceptions on purpose: that wait is their assertion, so they delete everything and return, the first 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` and the 45 s one in `ShouldNotifyClosedConnectionQUIC` do. A test that forks (`ShouldNotifyClosedConnectionQUIC`, `ShouldShareSelfSignedCertificateQUIC`) does so before `ThreadPool::Start()` or anything QUIC, and a parent that must stay fork-safe uses `alarm()` as its watchdog. 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 (`<sys/socket.h>`, `<msquic.h>`, `<openssl/*.h>`) 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 `<Thing>Exception` or `<Codec>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-<n>` for issue work, `claude/<topic>` or `fix/<topic>` 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, both made in-process by `:TLS`.** `QUICServerCredentials::selfSigned` is kept per user at `$XDG_CACHE_HOME/crafter-network/quic-cert/` (else `~/.cache/...`, else `$XDG_RUNTIME_DIR/...`), reused across restarts until a day before it expires so a browser's pinned hash survives a restart, and shared by processes that start together through a record lock on `lock` there; the listener hands msquic the pair as an in-memory PKCS#12, not the paths. `TLSServerCredentials::selfSigned` is new every start and carries `keyEncipherment` the QUIC one lacks. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second. To start the QUIC one cold, delete that directory.
- **`:TLS` declares module-internal functions** (`MakeWebTransportCertificatePem`, `CertificatePairExpiry`, `EncodePkcs12`, `CertificateSha256`) without `export`: `:ListenerQUIC` imports `:TLS` for them so libssl stays behind one partition, and a consumer never sees them.
- **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`. `OnClosed`, the native `QUICStream::SendAsync` callback and `OnRecieve`'s callbacks are the exceptions: they do run on the worker, `OnClosed` inside `SHUTDOWN_COMPLETE` after the handle is closed (a pool task only when set after the close), `onSent` inside the send's `SEND_COMPLETE` (a pool task only for a send msquic never took), `onData`/`onEnd` inside `RECEIVE` and the shutdown events (on the caller's thread for what `OnRecieve` finds waiting), so that none depends on a pool the host shares. A blocking call inside any of them stalls every connection on that worker.
- **`~ClientQUIC` lets the streams already stopped finish, up to 1 s, before `ConnectionShutdown`,** which aborts every stream and whatever msquic had not sent of it: a server that replied with `SendAsync(..., finish = true)`, let the stream go and deleted the connection lost the reply every time in `ShouldFinishStoppedStreamsQUIC` before. Streams count as finishing from `QUICStream::Stop` (a stream that can send) to `SHUTDOWN_COMPLETE`, in the registry's `finishing`.
- **`~ClientQUIC` waits for the connection's `SHUTDOWN_COMPLETE`, up to 1 s, before `ConnectionClose`.** msquic sends `CONNECTION_CLOSE` from a `FLUSH_SEND` it queues while processing `ConnectionShutdown` and ignores once `ShutdownComplete` is set; a close queued right behind the shutdown completed it silently, and the peer only timed the connection out (`fix(quic)` in `git log`, `ShouldTellPeerOfCloseQUIC`). So a destructor on an msquic worker thread (inside a stream callback) blocks that second and then some; only `OnClosed`'s, which has nothing left to close, is free.
- **`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**: `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 connects on either family (`QUIC_ADDRESS_FAMILY_UNSPEC`), and `ListenerQUIC(port, ...)` listens on both. `ListenerQUIC(address, port, ...)` listens on that address only, but msquic binds the dual-stack wildcard UDP socket for the port anyway and filters in `QuicBindingGetListener`: a connection to another local address is closed with `no_application_protocol` (the client sees `0x5c` in milliseconds), not ignored, and no other UDP socket can take that port on another address.
- **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 <n>: 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.