The README gains a QUIC section, which it never had: plain QUIC under an ALPN of your own, starting with a listener on port 0 and Port(). The consumer notes describe Port() and point tests at port 0 rather than at binding a socket by hand; the agent notes count the new test and make port 0 the default for a new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
28 KiB
AGENTS.md
Guidance for coding agents and contributors working on Crafter.Network itself. The README is the user-facing manual, and 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 imports 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(astd::system_errorwith theSocketPhasethat failed;ResolverCategory()forgetaddrinfo's codes), the receive semantics (what arrived, never a padded buffer) and theonErrorof the async receives. The listener hands its callback anew ClientTCPthe callback owns.:Stream:ByteStream(ReadSomeandWritewith a timeout per call,Shutdown,Descriptor,Protocol,Secure) andPlainStream, so one HTTP/1.1 implementation serveshttp://andhttps://.SetNonBlocking,PollDescriptor.:TLS:TLSContext,TLSStream, both credential structs,GetSelfSignedCertificatePem. OpenSSL stays behind theImplpointers; no OpenSSL header reaches an importer.:HTTP:HTTPRequest,HTTPResponse, theCreate*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; whatClientHTTP1::Fetchfollows 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 aByteStream&. The listener is aListenerTCPwith a thread per accepted connection; the client keeps one pooled connection with a replay-once rule, andClientHTTP1::Fetchis the URL fetch that follows redirects by RFC 9110 §15.4 with a client per hop (FetchOptions,FetchResult,FetchStop).:ClientQUIC:ClientQUICandQUICStreamfor both ends of a connection,QUICSettings, both QUIC credential structs,QUICException,QUICClosedException. The native unit holds the process-wideMsQuicRuntime(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 fourListen*variants, the self-signed certificate (made by theopensslCLI 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:ClientQUICand:ListenerQUIC. The listener demuxes every bidirectional stream into an HTTP/3 request or a WebTransport data stream and accepts extended CONNECT whenwtRoutesis 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 pulled 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: 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-<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
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: 31 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>; theSendRecieve*andInterop*families end inHTTP1orHTTPS1on the HTTP/1.1 side andHTTPorQUICon the QUIC side, andShouldSend,ShouldNotDropEarlyStreams,ShouldFallbackUnknownRoutesandShouldSurviveConnectionChurncarry no suffix. Openmain.cppwith a comment on the behaviour under test and the failure it guards against;tests/ShouldRecieveTCP/main.cppandtests/ShouldSurviveConnectionChurn/main.cppshow the shape, and a regression test names the bug. - A
Check(ok, what)helper in an anonymous namespace counts failures and prints them (PASS/FAILlines in the newer tests), andmainreturns 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), andShouldBuildWithoutQUICandShouldSendbind none. A new test binds port 0 and readsPort()(ListenerTCPandListenerQUICboth have it); a fixed port is for when it cannot. ThreadPool::Start()first, for anything that enqueues: every*Asynccall,ClientHTTP(its constructor installs anOnStreamdrain that runs as pool tasks),ListenerHTTP,ListenerQUIC, and the*Async-dispatchListen*variants ofListenerTCP;ListenerHTTP1and the sync TCP listeners need none, which is why ten tests never call it.Start()runshardware_concurrency() - 1workers andStart(1)none at all (tasks then wait for aJoinPool()nobody calls), so don't useStart(1)to make callbacks deterministic.- A QUIC test ends in
std::_Exit(code), afterstd::fflush(stdout)since_Exitflushes nothing (ShouldReportClosedQUICStreamdoes; 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 frommainwaits for the idle timeout.ShouldSurviveConnectionChurnis the exception on purpose: that wait is its assertion, so it deletes everything and returns, under a watchdog thread that callsstd::_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 inShouldNotDropEarlyStreamsdoes. Any test that can hang gets one. - Credentials:
QUICServerCredentials{ .selfSigned = true }againstQUICClientCredentials{ .insecureNoServerValidation = true }; for TLS,TLSServerCredentials{ .selfSigned = true }against a client givenGetSelfSignedCertificatePem().certificateascaPem, so verification is exercised rather than skipped. - A
ClientTCPdrives the wire whereClientHTTP1would hide the thing under test (pipelining inShouldSendRecieveHTTP1, garbage and a stalled peer inShouldSurviveAbuseHTTP1), and raw POSIX sockets (headers before the imports) whereClientTCPitself is under test (ShouldRecieveTCP,ShouldConfigureTCP,ShouldBuildWithoutQUIC). - The outside world:
curl,python3 -m http.serverand theopensslCLI are probed withcommand -v, and a missing one skips that half with a line saying so.ShouldSendis 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-onlyand//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.cpphas stray text after its copyright line; fix it when you touch the file.) - Four-space indentation in new code.
Crafter.Network-ClientTCP.cppm,-ListenerTCP.cppmand parts of-ClientTCP.cppand-ListenerTCP.cppare tab-indented from the first version; match the block you are in and do not reindent files, it buries the real diff.Crafter.Network.cppmandCrafter.Network-ListenerTCP.cppmare 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 aftermodule;, and in an interface only under#ifndef CRAFTER_NETWORK_BROWSER, since the wasm sysroot has none of them. Tests#includePOSIX headers before their imports the same way.PascalCasefor types and functions,camelCasefor variables and members,k-prefixed constants in the codecs (HTTP3::kFrameHeaders). Exceptions are<Thing>Exceptionor<Codec>ProtocolError.std::printlnfor output in new code; some tests still usestd::cout.- Spellings that are API:
Recievein every receive method and test name, and theListenSyncSyncfamily. 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,MakeBidiHandlerin-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
*Asynccall reports through its callback (onError(std::exception_ptr)on TCP,bool sentand the empty chunk on QUIC streams, an error string on the HTTP clients), because an exception escaping aThreadPooltask 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,QUICSettingson QUIC, andstd::chrono::millisecondseverywhere. A new blocking path gets a bound.
Design rules
Decided deliberately; change them only with the maintainer's say-so.
- One source, three builds. Native,
--no-quicand the browser are the same files, with#ifndef CRAFTER_NETWORK_BROWSERaround whole partitions,#ifndef CRAFTER_NETWORK_NO_QUICin the umbrella, and two-Browserimplementation units. No per-platform directory and no host-side#ifdefbeyond those two. - The browser is a client, and async. No listener, no raw TCP and no
*Syncmethod there; the JS bridge drivesfetch()andWebTransportand calls back through wasm exports. An API that cannot be made async does not join the shared surface. - 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, andShouldFallbackUnknownRoutesreplays one table over both to keep it so. fallbackis a hook, not a pattern syntax. Consumers bring their router; the library never grows a second route table that can disagree with it.- 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.
https://is a constructor argument, not a class. The HTTP/1.1 code holds aByteStream&and never learns whether it is aPlainStreamor aTLSStream;:TLSis protocol-agnostic and keeps OpenSSL out of the interface.- 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.cnfbroke inside the Flatpak;:TLSlinks the samelibssl. Where that OpenSSL is too old the answer is--no-quic, not a second TLS stack. - msquic is pinned, Crafter.Thread is not. Externals from outside Catcrafts get a
commit; sibling Catcrafts repositories track their default branch on purpose. - 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.
- 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.
- msquic handles are refcounted, and streams drain before their connection closes.
QUICStream::Stopinitiates, the asynchronousSHUTDOWN_COMPLETEcloses,~ClientQUICwaits (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 hangsRegistrationCloseat exit. - This project's flags stay here.
--no-quicis 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>orfix/<topic>otherwise) and open the PR againstmasteron the project's Forgejo atforgejo.catcrafts.net/Catcrafts/Crafter.Network.originis 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, withfeat,fix,docsortestand 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 logforfix(quic)shows the voice), and aBreaking:paragraph when a signature changes. Add aCo-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.mdwhen a signature, a default or a rule consumers lean on changes; consumers pick the notes up on their next build. crafter-build testandcrafter-build test --no-quicgreen before pushing, and the wasm build when the browser surface was touched. There is no CI.- Never commit
bin/,build/,AGENTS.mdor the example's build outputs.examples/SimpleClient/cert-hash.txtis tracked becausecfg.filesneeds it to exist; the native server overwrites it at runtime, and a changed hash in a diff is noise.
Gotchas
ThreadPool::EnqueuebeforeThreadPool::Start()is a null dereference, and most of the library enqueues: every*Async, every QUICOnStreamandOnDatagramdelivery,ClientHTTPandListenerHTTPentirely. The crash has no message; in a test it shows as 💥.- Two self-signed certificates.
QUICServerCredentials::selfSignedis made by theopensslCLI 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/tmpgoes away with its last instance (see "Dev environment");TLSServerCredentials::selfSignedis made in-process by:TLSand is new every start.GetSelfSignedCertificatePath()is the first,GetSelfSignedCertificatePem()the second. ComputeCertificateHashSHA256and the QUIC certificate shell out toopenssl(std::system,popen) and throwQUICExceptionwithout the CLI.:TLScould 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
mainwith 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)ingit log, with theSHUTDOWN_ON_FAILstory). - Callbacks the interfaces say run "on the msquic worker" run as pool tasks:
ClientQUIC::OnStreamandOnDatagramboth enqueue. What arrives before a handler is set is queued: streams are delivered onOnStream; datagrams go toRecieveDatagramSync's queue and never to a laterOnDatagram. ListenerQUIC::ListenAsync*run the accept loop on astd::thread, not the pool as their comments say, andListenerHTTP::ListenisListenSyncAsyncunderneath, so aListenerHTTP'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
ClientQUICagainst an HTTP/3 server must drain the server's unidirectional streams (control, QPACK encoder and decoder) with anOnStreamreader, or they back up and msquic may abort the connection.ClientHTTPdoes it; a hand-rolled WebTransport client (ShouldEchoWebTransport) does it itself. QUICSettings::peerUnidiStreamCountis the first thing to raise when a peer stalls opening streams; it was a hardcoded 16 untilShouldHonourQUICSettings.- The comments about
-fno-exceptionsinproject.cpp, the umbrella and the browser unit are history. crafter-build 4.0.0 compiles wasm with exceptions on and refuses-fno-exceptions;:HTTP3stays out of the browser build and the browser units still report through callbacks because the JS side has no exception channel, not becausethrowwould not compile. - The header comment of
Crafter.Network-HTTP3.cppmis stale: the partition is exported from the umbrella natively and has a Huffman decoder. The README is right. - More comments the code outgrew:
-ListenerQUIC.cppsays the certificate pair lives in amkdtemp'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.cppmcallsselfSignedan in-memory certificate (it is on disk, made by the CLI);ListenerHTTP.cppmsaysListen()dispatches on the calling thread (it isListenSyncAsync); the link-flag comment inproject.cppstill has msquic bundling quictls (it links the systemlibssl). 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::concurrentClientLimitis stored and never read; onlytotalClientLimitends 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.ClientTCPresolves 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 <n>: Address already in useon the HTTP/1.1 side and aListenerStart failedQUICExceptionon 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.