Skip to content

CI: Run the mod_http3 tests - #945

Draft
mbeijen wants to merge 13 commits into
apache:trunkfrom
mbeijen:h3-ci
Draft

mbeijen wants to merge 13 commits into
apache:trunkfrom
mbeijen:h3-ci

Conversation

@mbeijen

@mbeijen mbeijen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

This adds a mod_http3 job on Ubuntu 26.04 (OpenSSL 3.5, packaged libnghttp3) that builds with --enable-http3 and runs test/modules/http3. The runner's curl has no HTTP/3, so TEST_CURL_H3 builds a curl release with OpenSSL QUIC and libnghttp3 into the cached ~/root. PYHTTPD_STRICT_OPTIONAL makes a missing mod_http3 or HTTP/3 curl fail the job instead of skipping the suite.

The job only passes with #938, #939, #940, #941, #942, #943 and #944 in, so this branch carries copies of them. I'll rebase it once those are merged.

  • This PR does not report or address a security vulnerability (see SECURITY.md and https://www.apache.org/security/#reporting-a-vulnerability).
  • Code follows the httpd style guide.
  • New log messages (level debug or higher) use an empty APLOGNO() tag; numbers are assigned by a committer at merge time (see docs/log-message-tags/README).
  • If the change is user-visible, a changes-entries/*.txt file is included, following the template in README.CHANGES (not needed otherwise).
  • Test cases based on pyhttpd are included where appropriate, in the test/modules/xxx directory matching the modules/xxx module source, or test/modules/core for core server changes. On Unix, verify these pass locally with e.g. make check-pytest PYTEST_DIRS=test/modules/xxx.

test/modules/http3 runs requests over HTTP/3 with curl --http3-only
against a QUIC listener on the https port number. It covers static
files, HEAD, 404, a 1 MB response, several requests on one connection,
virtual host selection by Host, POST content to CGI up to 1 MB and the
CGI environment.

The package is skipped when mod_http3, mod_ssl or curl's HTTP3 feature
is missing.

Generated-by: Claude Code (Claude Opus 5.5)
stream_worker() built the request_rec and went straight to
ap_process_request(), with only ap_update_vhost_from_headers() in
between.  It skipped the two steps that HTTP/1.1 (ap_read_request()) and
HTTP/2 (h2_create_request_rec()) run on every request:

* ap_check_request_header(): vhost selection with StrictHostCheck, the
  missing Host check and Expect handling.
* ap_post_read_request(): the post_read_request hooks of mod_setenvif,
  mod_remoteip, mod_headers ("early"), mod_proxy (forward proxy
  detection), mod_md, mod_unique_id and others, plus the core checks on
  the complete request.  mod_http3's own h3_hook_post_read_request never
  ran for its own requests either.

So configuration behaved differently over HTTP/3, including access
control: with "SetEnvIf User-Agent ^evilbot bad_bot" and
"Require not env bad_bot", a request was denied with 403 over HTTP/1.1
but served with 200 over HTTP/3.  Both now give 403.

Add test_003_access.py to the mod_http3 tests for this.

Generated-by: Claude Code (Claude Opus 5.5)
configure checked for nghttp3_conn_server_new(), which nghttp3 1.x only
provides as a macro around nghttp3_conn_server_new_versioned(), so the
link test failed and --enable-http3 always reported "nghttp3 library is
unusable".  Check for the versioned function instead.

h3_io.c calls nghttp3_conn_is_drained2(), which was added in nghttp3
1.16.0 while configure accepts nghttp3 >= 1.0.0.  Check for it and fall
back to nghttp3_conn_is_drained(), which has the same semantics, so the
module builds against e.g. the nghttp3 1.8.0 in Debian 13.

Generated-by: Claude Code (Claude Opus 5.5)
OpenSSL before 3.5.7 frees a stream's send buffer once the peer
acknowledges the FIN sent by SSL_stream_conclude(), and
SSL_get_stream_write_state() then dereferences it (fixed upstream by
"Make SSL_get_stream_write_state() safe for concluded streams").
Children crashed this way in drain_one_stream() on Ubuntu 26.04, which
ships OpenSSL 3.5.5.

Mark a stream through its SSL app data when it is concluded, and treat
its sending half as finished without asking OpenSSL.

Generated-by: Claude Code (Claude Opus 5.5)
The threads of mod_http3 allocated from the child pool, which is not
thread-safe, and kept running while it was destroyed:

- The port acquirer thread, in every child that does not own the UDP
  port, created a socket in the child pool every 200 ms, racing with the
  main thread and never freeing it.
- Under prefork the child pool's allocator has no mutex and is shared
  with the request pools, which the event thread raced with when it
  created connection pools.
- prefork does not run the child_stopping hook when a child exits from
  a signal handler, so neither thread was stopped before the child pool
  was destroyed and exit() ran OpenSSL's cleanup under them.

These crashed children with SIGSEGV on stop, mostly under prefork.

Give mod_http3 a pool of its own with a locked allocator, give each port
acquisition attempt a scratch pool that is cleared on retry, and stop the
threads from a pre-cleanup of the child pool as well.

Generated-by: Claude Code (Claude Opus 5.5)
When a child stops, connections still open after the GOAWAY grace
period are shut down with SSL_SHUTDOWN_FLAG_RAPID. That flag skips the
terminating period, but OpenSSL still waits for all stream data to be
acknowledged first. The GOAWAY just sent to a client that has already
gone away never is, so the event thread waited for the idle timeout and
the parent killed the child with SIGKILL (AH00046).

Add SSL_SHUTDOWN_FLAG_NO_STREAM_FLUSH to the rapid shutdown.

Generated-by: Claude Code (Claude Opus 5.5)
On a stop, every connection waited the full GOAWAY grace period of
H3_GOAWAY_GRACE_SECS, even when no request was in flight:
nghttp3_conn_is_drained() only reports a drained connection after
nghttp3_conn_shutdown(), and that was only called once the connection
was drained or the grace period was over. A child with an idle HTTP/3
connection therefore outlived the parent's first wait and was sent
SIGTERM (AH00045).

On an ungraceful stop, call nghttp3_conn_shutdown() right after the
shutdown notice, so that new streams are refused and is_drained reports
only the streams in flight. Idle connections close at once; requests in
flight still get the grace period.

Generated-by: Claude Code (Claude Opus 5.5)
Add a "mod_http3" job on Ubuntu 26.04, which has OpenSSL 3.5 and
libnghttp3, that builds with --enable-http3 and runs modules/http3.

The runner's curl has no HTTP/3, so TEST_CURL_H3 builds the given curl
release with OpenSSL QUIC and libnghttp3 into the cached ~/root, and
the run points pyhttpd at it through $CURL, failing early when that
curl lacks HTTP3. The job sets PYHTTPD_STRICT_OPTIONAL, so a missing
mod_http3 or HTTP/3 curl fails it instead of skipping the suite.

Generated-by: Claude Code (Claude Opus 5.5)
@mbeijen
mbeijen marked this pull request as draft November 4, 2026 17:39
Review feedback: define H3_NGHTTP3_CONN_IS_DRAINED() for both nghttp3
versions instead of redefining the lowercase nghttp3 function name.

* modules/http3/h3_io.c: Add H3_NGHTTP3_CONN_IS_DRAINED() and use it.

Generated-by: Claude Code (Claude Opus 5.5)
Review feedback: the HAVE_NGHTTP3_CONN_IS_DRAINED2 check existed only
in the autoconf build, so CMake builds always fell back to the old
function. Test NGHTTP3_VERSION_NUM instead, which every build gets
from <nghttp3/version.h>. Also keep the declaration of "drained" at the
top of its block, as C89 requires.

* modules/http3/config2.m4: Drop the nghttp3_conn_is_drained2 check.
* modules/http3/h3_io.c (H3_NGHTTP3_CONN_IS_DRAINED): Use
  nghttp3_conn_is_drained2() from nghttp3 1.16.0 on.
  (service_session_pass): Declare "drained" at the top of the block.

Generated-by: Claude Code (Claude Opus 5.5)
Review feedback: keep the body of stop_child_threads() in
h3_c1_child_stopping(), which the child pool cleanup now calls, so no
forward declaration is needed. Move the pool creation to h3_threads.c so
other code can reuse it.

* modules/http3/h3_server.c (h3_c1_child_stopping): Fold in
  stop_child_threads().
* modules/http3/h3_threads.c, modules/http3/include/h3_threads.h
  (h3_thread_safe_pool_create): New, from create_h3_pool() in h3_server.c.

Generated-by: Claude Code (Claude Opus 5.5)
Review feedback: the mutex on the allocator of the HTTP/3 pool covers
subpools, not allocations from the pool itself. h3_io_listen_start()
registered its pool cleanup after starting the event thread, which
allocates from the same pool, so the two could allocate at once when
the port acquirer thread started listening. Register it first.

Also destroy the pool when its mutex cannot be created, log when the
port acquirer cannot create its pool, drop an empty APLOGNO() that no
other message in the file has, and keep the declarations in
h3_child_init() at the top of their blocks, as C89 requires.

* modules/http3/h3_io.c (h3_io_listen_start): Register the cleanup
  before starting the event thread.
* modules/http3/h3_server.c (port_acquire_thread_fn): Log the pool
  creation failure.
  (h3_child_init): Declare variables first; drop the APLOGNO().
* modules/http3/h3_threads.c (h3_thread_safe_pool_create): Destroy the
  pool if its mutex cannot be created.
* modules/http3/include/h3_threads.h: Say what the mutex does not cover.

Generated-by: Claude Code (Claude Opus 5.5)
# Conflicts:
#	modules/http3/h3_threads.c
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