OpenSSL's socket BIO writes with write(2) rather than send(2), so unlike
PlainStream it cannot pass MSG_NOSIGNAL. Writing to a peer that is gone
therefore raised SIGPIPE, and with the default disposition that takes the
whole process down.
This is not an edge case. It fires on any teardown where the far side
closed first, because SSL_shutdown still tries to put a close_notify on
the wire — which is exactly what ShouldSendRecieveHTTPS1 does when it
drops a client whose certificate check failed. The test died on SIGPIPE
with every assertion passing.
Installing a process-wide SIG_IGN would fix it by changing how the
caller's own writes report failure, which a library has no business
doing. SIGPIPE from write(2) is delivered to the writing thread, so block
it for that thread across each OpenSSL call instead and drain any pending
instance before unblocking. A caller who already blocks SIGPIPE is left
untouched — a pending signal there may be theirs to consume.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HTTP/1.1 was plaintext-only, which left `https://` to either an HTTP/3
listener or a terminating proxy in front. Neither helps the callers this
stack exists for — curl scripts, CI tooling, old proxies — so wrap the
transport in libssl instead.
Two new partitions:
:Stream a ByteStream with per-call deadlines on both directions, plus
the plaintext socket implementation. The HTTP/1.1 client and
listener now hold a ByteStream& and never learn which
transport they have, which is what lets one code path serve
both schemes.
:TLS TLSContext/TLSStream over OpenSSL 3, with credentials for both
roles: chain and hostname verification on by default, private
trust anchors, client certificates, mutual TLS, ALPN, and an
in-process self-signed certificate for development.
Both descriptors go non-blocking and every read and write is driven by
poll() against a deadline. That is required for TLS — a blocking
descriptor cannot express a handshake timeout — and it means a plaintext
write can now time out too, instead of parking forever against a peer
that stopped reading.
ClientHTTP1 and ListenerHTTP1 gain credential-taking constructors; the
existing ones still speak http://. The listener handshakes on the
connection's own thread, so a peer that stalls mid-handshake costs one
thread rather than the accept loop, and a failed handshake is counted
rather than logged — on a public port it is ordinary traffic.
MessageParser gains SetDefaultScheme so origin-form targets report the
scheme the transport actually used; handlers shared with ListenerHTTP now
see the same "https" they would over HTTP/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>