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>
One request to an http(s) URL with redirects followed by RFC 9110 §15.4:
301, 302, 303, 307 and 308 for every method; a 303 makes the request a
GET without its body and content headers, a 301 or 302 does so for a POST
alone, a 307 or 308 keeps the method and the body. The Location is
resolved against the sending URL by RFC 3986, a hop between http and
https is allowed, Authorization, Proxy-Authorization and Cookie are
dropped on a change of origin, and each hop is a client of its own, since
one ClientHTTP1 is one origin. A redirect without a usable Location, to
another scheme, back to a URL already visited or past maxRedirects comes
back as the response, with `stop` saying why.
The new :URL partition is the URL type behind it, exported: Parse,
Resolve (RFC 3986 §5.2; its §5.4 examples are the test), ToString, and
the pieces a request is built from.
This replaces the FetchHTTP1 free function, which followed redirects the
way Python's urllib did (its repeat limit, its quoting of the Location, a
POST turned back on a 307) for a port that no longer needs the fidelity.
Breaking: FetchHTTP1, FetchOptionsHTTP1 and FetchResultHTTP1 are gone;
use ClientHTTP1::Fetch, FetchOptions and FetchResult, whose `url` is a
URL and whose `limited` became `stop`.
SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
Send() and the receives threw a bare std::runtime_error with the
strerror text, so a caller that reports the cause the way Python does -
"the connection broke ([Errno 104] Connection reset by peer)", which a
language model reads in Crafter.Review's SQL helper output - could not
tell the code. They throw SocketErrorException now, as a failed connect
already does: kind System, the errno, and the same message, still a
std::runtime_error. A receive timeout stays SocketTimeoutException and
a closed peer SocketClosedException.
ShouldReportIoErrors: a peer that resets the connection (SO_LINGER 0)
fails the next receive with ECONNRESET and a send into it with EPIPE
or ECONNRESET.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ClientHTTP1 gains connectTimeout (default 15 s), applied to each
address the host resolves to. `timeout` only ever bounded the waits for
a response, so a host that drops the SYNs held Send() for the kernel's
retries, about two minutes, whatever the caller had asked for.
- The default Host header puts an IPv6 literal in brackets.
- FetchHTTP1(url, options) makes one request to an http:// or https://
URL and follows redirects as Python's urllib does, so urllib code ported
over keeps its behaviour: 301/302/303/307/308 for GET and HEAD,
301/302/303 for POST (re-sent as a GET without its body), a relative
Location resolved as urljoin resolves it and percent-encoded as urllib
quotes it, http and https hops mixed, each redirect's body read before
the next request, and the chain stopped by urllib's limits (4 repeats
of one URL, 10 different URLs) with the last redirect returned and
`limited` set. Proxies are not consulted.
New: ShouldTimeoutSilentHTTP1, ShouldFollowRedirectsHTTP1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new constructor takes a numeric bind address (127.0.0.1, ::1, 0.0.0.0,
::) and accepts port 0, and Port() reports the port the listener got.
Until now every listener bound 0.0.0.0 at a port the caller chose, so a
helper meant for this machine's own processes was reachable from the
network, and the caller had to guess a free port. A name such as
`localhost` is refused instead of bound to whichever address the
resolver ranks first. The port-only constructor still listens on every
IPv4 interface.
`running` is atomic now: Stop() runs on another thread than the accept
loop that reads it. The accept loops take a sockaddr_storage, so an IPv6
peer's address is no longer cut short.
New: ShouldBindLoopbackEphemeral.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ClientTCP resolves with AF_UNSPEC and tries every address the name
resolves to, in getaddrinfo's order, the way Python's
socket.create_connection does. It used to ask for AF_INET only and keep
the first answer, so `localhost` could not reach a server listening on
::1 alone, which is what a dev server on a recent Node does. The connect
timeout bounds each address on its own; when none connects, what the
last one failed with is thrown.
- A failed resolve, socket() or connect throws SocketErrorException: the
std::runtime_error it always was, with the same message, plus the errno
(SocketErrorKind::System) or the getaddrinfo code
(SocketErrorKind::Resolver). A caller that reports the cause in its own
words, such as a Python-style "[Errno 111] Connection refused", no
longer has to parse it back out of the text.
- The move constructor carries the resolved addresses along.
New: ShouldConnectDualStack, ShouldReportConnectErrors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with
them msquic: a consumer that only needs TCP builds no msquic. The flag
isn't passed on to dependencies.
- --local takes Crafter.Thread from the sibling checkout, as the other
Crafter projects do.
- ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and
SocketTimeoutException.
- TCP receives return the bytes that arrived:
- RecieveSync(n) returned the whole buffer when fewer had come;
- RecieveUntilCloseSync truncated what it had read;
- a signal (EINTR) during a receive threw where it should wait on;
- RecieveSync(size, buffer) returns the count and throws like the others;
- a receive of no bytes returns at once: recv's 0 for it isn't a close.
- Every TCP *Async receive takes an onError(std::exception_ptr): an
exception escaping a ThreadPool task ended the program. An empty onError
drops the error.
- QUIC streams:
- async receives deliver an empty chunk on close, as the browser build does;
- RecieveUntilFullAsync gives what arrived on a stream that closes early;
- RecieveUntilFullSync takes all it asked for or nothing, under one hold
of the lock, so what did arrive stays for the next receive and no
other receive can take bytes from among it;
- SendAsync's callback hears whether the send went out (bool sent): native,
browser, the JS bridge and the example.
- msquic is pinned to v2.6.3 and built against the system OpenSSL
(QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's
openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with
config_diagnostics on) made it fail to initialise. The README says to use
--no-quic where the distro's OpenSSL is older.
- Tests now register on any Linux triple: they never ran under the Flatpak's
x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP,
ShouldRecieveTCP, ShouldReportClosedQUICStream.
Breaking: SendAsync's callback takes a bool, the TCP *Async receives take
an onError, and RecieveSync(size, buffer) returns the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other QUIC test ends in std::_Exit(0), so nothing in the suite ever
ran a ~ClientQUIC. This adds 25 build-up/tear-down cycles with reader
threads parked inside RecieveSync -- the shape a reconnect loop has --
which aborts on ~10/20 runs against the current implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The HTTP/1.1 section promised the opposite of what the code now does — a
"No TLS" heading stating there was no plan to link a TLS stack into this
path. Replace it with what to actually pass, and lead with the part that
gets deployed wrong: verifying the chain without the hostname is not a
check, and a private trust anchor is the answer for a self-signed peer
rather than insecureNoServerValidation.
Also document :Stream and :TLS as modules in their own right — TLSStream
is a ByteStream over any descriptor, not something only HTTP can use —
and record libssl as a system dependency, including why it is not
vendored the way msquic is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Routes that cannot be enumerated" section, linked from both
listener sections since the hook and its precedence are identical on
each. Also records the HTTP/3 query-strip change and why a fallback has
to be a constructor argument on the ListenerAsync* wrappers.
README: HTTP/1.1 in the intro, feature list, module list, browser-build
exclusions, dependencies and test list, plus a Components section
covering both classes, the standalone codec, what is and is not
implemented, the smuggling-shaped inputs that are rejected, and an
explicit note that this path is plaintext and belongs behind a TLS
terminator.
ClientHTTP1::timeout was hard-coded and invisible; make it a public
member alongside `limits`, mirroring the listener's timeouts.
Also pipelining coverage in ShouldSendRecieveHTTP1: two requests written
before either is answered, driven from a raw socket since ClientHTTP1
waits for each response.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>