Skip to content

H3 consolidation: merge client and server code - #9

Merged
pgit merged 8 commits into
masterfrom
h3-consolidate
Sep 1, 2026
Merged

pgit merged 8 commits into
masterfrom
h3-consolidate

Conversation

@pgit

@pgit pgit commented Aug 31, 2026

Copy link
Copy Markdown
Owner

No description provided.

pgit and others added 8 commits August 30, 2026 18:25
The QUIC/HTTP3 server and client were two near-identical files: the read
path, the write path, flow control, the ngtcp2/nghttp3 callback bridges
and the write loop existed twice, and every fix had to be applied twice,
in parallel, to both.

A QUIC connection is symmetric, and so is an HTTP/3 stream -- what
differs between the roles is only which half of the exchange travels in
which direction. So both now share one implementation, phrased in terms
of the *incoming* and the *outgoing* message:

* http3::Http3Stream (anyhttp/http3_stream.hpp, src/http3_stream.cpp) --
  one request/response exchange: read path, write path, header parsing,
  lifecycle, plus the Http3Reader/Http3Writer adapter templates. Role
  hooks: on_pseudo_header(), on_headers_complete(), on_failed(),
  submit_response().
* http3::Http3Session (anyhttp/http3_session.hpp, src/http3_session.cpp)
  -- one QUIC connection: every callback bridge, write_pkt/write_streams,
  the timers, flow control, setup_http3(), setup_tls(). Role hooks:
  handle_error(), send_datagrams(), make_stream(), on_http3_ready(),
  on_new_cid()/on_remove_cid().
* anyhttp/http3_common.hpp -- make_nv(), log_headers(), the ngtcp2 log
  callback and the shared constants.

What is left in server_impl_udp.cpp is what is genuinely server-side:
the TLS server context, the UDP demux (many connections over one socket,
a CID table, a strand and a dup()ed fd per session) and the
closing/draining bookkeeping. In client_impl_udp.cpp: the TLS client
context, the connect()ed socket and its receive loop, wait_ready() and
async_submit(). 4902 lines became 3900, of which 2250 are now shared.

Making the two sides one implementation also made them behave alike:
the client now uses the server's ngtcp2_conn_write_aggregate_pkt2() write
path (splitting the buffer by gso_size, without UDP_SEGMENT), both roles
register nghttp3's acked_stream_data, both send STOP_SENDING when a
reader is dropped early, both support read cancellation, and stream
failure reporting goes through one Http3Stream::fail().

The one thing that stayed apart is how a body is handed to nghttp3,
now spelled out as WriteMode: the server points nghttp3 straight into
the caller's buffer and completes the write on acknowledgement, while
the client stages through a bounded copy so that cancelling a write
leaves the stream intact. Everything around it is shared.

Three bugs found on the way, fixed here:

* async_get_response() on a stream that had already failed waited
  forever, because only a handler installed *before* the failure was
  ever completed. The stream now remembers why it died and answers a
  late caller right away.
* Write completions ran the application from inside a nghttp3 callback,
  i.e. from inside ngtcp2: a handler destroying the session there wrote
  a CONNECTION_CLOSE with a fresh timestamp and the enclosing write pass
  then continued with its own, stale one, tripping ngtcp2's
  "conn->log.last_ts <= ts" assertion. Completions are posted now, and
  write_pkt() stops packing when the connection went away underneath it.
* Delivering a response can drop the last reference to its stream, so
  async_get_response() holds one of its own (found by ASAN).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each protocol implementation now lives in files prefixed h1_ (beast),
h2_ (nghttp2) and h3_ (ngtcp2/nghttp3), and the protocol libraries no
longer leak out of them: <nghttp2/*> is included only by h2_* files,
<ngtcp2/*>/<nghttp3/*> only by h3_* ones.

Four places needed more than a rename:

* formatter.hpp pulled in <nghttp2/nghttp2.h> for the nghttp2_nv
  formatter, which moves to h2_common.hpp (absorbing nghttp2_common.hpp).

* server_impl.cpp / client_impl.cpp instantiated the h1 and h2 session
  templates themselves. They now go through make_server_session() /
  make_client_session(), declared in h1_backend.hpp / h2_backend.hpp in
  terms of asio types only and defined in the backends' own translation
  units, which are the only ones instantiating those templates.

* the server's ALPN callback called nghttp2_select_next_protocol(),
  replaced by a preference-ordered selection over the ALPN wire format.

* Server::Impl held the whole QUIC demux -- UDP socket, receive loop,
  connection-ID table, an ngtcp2_cid forward declaration. That is now
  Http3ServerImpl in h3_server.cpp, behind the abstract Http3Server and
  make_http3_server() in h3_backend.hpp. What is left in Server::Impl is
  m_http3 plus add_session()/remove_session(), the session registry all
  three protocols share.

Also drops the stray NGHTTP2 link from the server executable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The error_code overloads of shutdown()/set_option() now return void, so
drop the std::ignore = in front of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hand-written "[{}] " prefix and log_prefix_ argument with the
mlog* macros in the member functions of h3_session.cpp.

The static ngtcp2/nghttp3 callbacks keep the explicit form: mlog* expands to
an unqualified logPrefix(), which has nothing to bind to there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment wording and formatting only, no behaviour change:

- h3_session: half-open range as [dest, dest+destlen[
- h3_backend: reflow the Http3Server comment, "de-multiplexing"
- common: section separator before the ReadSome aliases
- cspell: sveccnt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pgit
pgit merged commit 76c44b0 into master Sep 1, 2026
4 checks passed
@pgit
pgit deleted the h3-consolidate branch September 1, 2026 14:22
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