Skip to content

async_get convenience wrapper, support custom methods - #17

Merged
pgit merged 9 commits into
masterfrom
headers
Sep 19, 2026
Merged

pgit merged 9 commits into
masterfrom
headers

Conversation

@pgit

@pgit pgit commented Sep 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

pgit and others added 9 commits September 17, 2026 21:31
async_get() submits the request, ends its empty body, waits for the response and
reads all of it, handing back a plain Beast message -- status, header fields and
the body as a std::string, aliased as client::Message. What a simple GET spread
over four steps is one asynchronous operation now, completion tokens and
cancellation included.

Sending an actual GET means the method can no longer be hardcoded: all three
backends built their request with ":method: POST", so Session::Impl::async_submit()
takes the method as a parameter. The public async_submit() still passes "POST",
so nothing else changes on the wire.

The request goes out with "Content-Length: 0" unless the caller frames a body
itself. That keeps HTTP/1.1 from making a bodiless request chunked, and with it
from counting the request as incomplete -- and the session as busy -- until the
body is ended.

The message that comes with an error is empty and says status::unknown, rather
than the 200 a default-constructed Beast response would claim.

test_get.cpp covers status, fields, body, request headers, an empty body, a 1 MiB
one, two requests in a row and cancellation, for all three protocols, plus the
request line as a raw HTTP/1.1 peer sees it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header, file handler and client testcases that send no request body and read
the response to its end say so in one line now. client::Message is a Beast fields,
so expect_contains() and values_of() take it unchanged.

FileHandler::get() was async_get() spelled out, and hands back the message itself
instead of a (status, body) tuple. HeaderLimits::request() loses its two error
checks along with the operations they belonged to.

These requests go out as GET with "Content-Length: 0" now, instead of as a chunked
POST -- which is what the testcases describe anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The constructor taking a buffer sequence ended its loop at buffer_sequence_begin()
rather than buffer_sequence_end(), so first and last were the same iterator and it
copied nothing at all: n_ and size_ stayed 0, leaving an empty buffer array.

Nothing ever instantiated it, which is why this never showed. AnyAsyncStream spelled
out the iterator pair constructor instead, and that one was correct. It passes the
sequence itself now, which is what the constructor is there for -- the two loops are
otherwise the same, both capping at N and skipping empty buffers.

The bug could not have survived being used: with buffer_sequence_begin() put back,
the HTTP/1.1 testcases hang, reading and writing zero bytes forever, as an empty
buffer sequence makes them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing functional, except that get_unused_port() now probes on the IPv6
loopback: its only caller connects to "localhost" and expects to be refused,
and that resolves to ::1 first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The TCP side built a fresh asio::ssl::context inside handle_connection(),
reading the PEM files from disk for every single connection, while HTTP/3 kept
its SSL_CTX in a function-local static for the lifetime of the process. Apart
from the per-connection cost, the two disagreed about when a rotated
certificate is picked up: regenerating the test PKI under a running server
broke HTTP/3 while HTTP/2 silently carried on with the new chain.

Both now hold their context for as long as the server lives -- Server::Impl for
TCP, Http3ServerImpl for QUIC -- so a regenerated PKI needs a restart either
way, and nothing is read from disk per connection.

Also adds anyhttp.org to the test server certificate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A session is a template over its stream, and there are four of those: a plain
tcp::socket, a TLS stream, beast's tcp_stream and the type-erased
AnyAsyncStream. Beyond the async read and write operations they already have in
common, a session needs the underlying socket -- to shut it down or close it --
and an executor to run its loops on, and neither is spelled the same way by all
four. Both h1_session.cpp and h2_session_details.hpp had their own private set
of get_socket() overloads for this, and the executor was passed in from
outside.

stream_traits<Stream> now provides get_socket() and get_executor() for all of
them, with a SocketStream concept and a free get_socket() on top. There is no
free get_executor(), because the classes calling these have a get_executor() of
their own that would hide it.

With the executor reachable from the stream, each backend's per-stream-type
factory overloads collapse into a single constrained template taking the stream
by rvalue reference and no executor argument -- ten declarations down to four.
The h2 h2c variant folds into the same template via an optional<Upgrade>, which
retires the three make_h2c_session() dispatch helpers in h1_session.cpp,
including the one that only existed to throw for TLS. The definitions stay in
the .cpp files, explicitly instantiated there, so beast and nghttp2 are still
confined to one translation unit each.

Taking the executor from the stream only holds if the stream is on the right
one to begin with. It was not: with Config::use_strand, tcp_accept_loop() made
the strand at co_spawn() time, leaving the socket on the plain executor. The
socket is now accepted onto the connection's executor, as the HTTP/3 side
already does, and handle_connection() is spawned on that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The type-erased stream, the TLS detection and the HTTP/2 preface detection are
internals of the server, not part of its interface, so they move to
include/anyhttp/detail/ -- h2_detect.hpp as detect_h2.hpp, to match the name of
the operation it declares. AnyAsyncStream becomes any_async_stream, like the
other stream types it stands in for.

Its implementation is now only forward declared. The buffer-erasing async
operations stay inline, but the buffer sequence is copied into the buffer_array
before async_initiate(), so the initiation can forward to an out-of-line member
instead of dereferencing the incomplete implementation.

The implementation itself lives in detail/any_async_stream_impl.hpp, which only
src/any_async_stream_impl.cpp includes. What used to be TestStream in
server_impl.cpp is a template over the stream now, taking its socket and
executor from stream_traits instead of assuming a bare one, and is explicitly
instantiated for a TCP socket and a TLS stream -- the same pattern the session
backends use. Callers get make_any_async_stream(), because converting a
unique_ptr to the base is no longer possible where the base is incomplete.

get_socket() returns TcpSocketBase rather than an ip::tcp::socket, which is what
a TLS stream can actually hand out and all a session needs for shutdown() and
close(). The alias moves next to the stream returning it, together with
SslStream, which the two backends had been spelling out separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pgit
pgit merged commit e70d52a into master Sep 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant