Skip to content

mod_http3/src/h3_io.c

Functions

Name
int h3_io_at_connection_limit(h3_io_t * io)
void teardown(h3_io_t * io)
apr_status_t h3_io_listen_start(apr_pool_t * pchild, server_rec * s, h3_server_conf * conf, int udp_fd)
void h3_io_listen_stop(h3_io_t * io)
void wait_for_event(h3_io_t * io)
void remove_pending_handshake(h3_io_t * io, int index, int free_conn)
apr_status_t spawn_serviced_session(h3_io_t * io, quic_conn * conn)
void progress_pending_handshakes(h3_io_t * io)
int prepare_accepted_connection(h3_io_t * io, quic_conn * conn)
int service_session_pass(h3_io_t * io, h3_session * session)

Attributes

Name
h3_io_t * child_h3_io

Functions Documentation

function h3_io_at_connection_limit

int h3_io_at_connection_limit(
    h3_io_t * io
)

Parameters:

  • io The h3_io_t instance to check.

Return: Non-zero if at the limit, zero otherwise.

Check if the active connection limit (H3MaxConnections) is reached.

function teardown

static void teardown(
    h3_io_t * io
)

function h3_io_listen_start

apr_status_t h3_io_listen_start(
    apr_pool_t * pchild,
    server_rec * s,
    h3_server_conf * conf,
    int udp_fd
)

Parameters:

  • pchild Child process pool.
  • s The server_rec this listener is associated with.
  • conf The vhost's h3_server_conf (cert/key paths, port).
  • udp_fd Pre-opened non-blocking UDP socket bound to the listen port.

Return: APR_SUCCESS on success, APR_EAGAIN if the port is already owned, or another APR error code.

Build the listener, bind the UDP socket via udp_fd, and spawn the event thread. Idempotent on the same port: returns APR_EAGAIN if another child already owns it.

function h3_io_listen_stop

void h3_io_listen_stop(
    h3_io_t * io
)

Parameters:

Stop the event thread, join all worker threads, and release the UDP fd and engine. Safe to call with NULL.

function wait_for_event

void wait_for_event(
    h3_io_t * io
)

Parameters:

  • io The listener whose socket and wakeup pipe should be polled.

Wait for network read/write events using poll().

function remove_pending_handshake

void remove_pending_handshake(
    h3_io_t * io,
    int index,
    int free_conn
)

Parameters:

  • io The owning h3_io_t listener instance.
  • index The index of the connection in the array.
  • free_conn If non-zero, the connection object is freed.

Remove a connection from the pending handshake array.

function spawn_serviced_session

static apr_status_t spawn_serviced_session(
    h3_io_t * io,
    quic_conn * conn
)

function progress_pending_handshakes

void progress_pending_handshakes(
    h3_io_t * io
)

Parameters:

  • io The owning h3_io_t listener instance.

Progress handshakes for all pending connections, timing out stalled connections and adding completed connections to the event loop.

function prepare_accepted_connection

int prepare_accepted_connection(
    h3_io_t * io,
    quic_conn * conn
)

Parameters:

  • io The owning h3_io_t listener instance.
  • conn The newly accepted QUIC connection instance.

Return: 1 on success, 0 otherwise.

Prepare a newly accepted connection before starting the handshake. Sets stream modes, Incoming Stream policies, and pushes it to the pending array.

function service_session_pass

int service_session_pass(
    h3_io_t * io,
    h3_session * session
)

Parameters:

Return: Non-zero when stream input made progress and another pass should run without polling.

Service the newly established session connection. Drives HTTP/3 request processing.

Attributes Documentation

variable child_h3_io

h3_io_t * child_h3_io = NULL;

Source code

/*
 * Copyright (c) 2026 The mod_http3 Project Authors. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <httpd.h>

#include <http_config.h>
#include <http_log.h>

#include <apr_allocator.h>
#include <apr_atomic.h>
#include <apr_pools.h>
#include <apr_portable.h>
#include <apr_thread_proc.h>
#include <apr_thread_pool.h>

#include <errno.h>
#include <poll.h>
#include <unistd.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "h3.h"
#include "h3_check.h"
#include "h3_io.h"
#include "h3_request.h"
#include "h3_session.h"
#include "h3_socket.h"
#include "h3_stream.h"
#include "h3_threads.h"
#include "h3_version.h"
#include "mod_http3.h"
#include "quic.h"

h3_io_t* child_h3_io = NULL;

int h3_io_at_connection_limit(h3_io_t* io)
{
    h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
    apr_uint32_t active = (apr_uint32_t)io->active_sessions->nelts + (apr_uint32_t)io->pending_handshakes->nelts;
    return active >= conf->h3_max_connections;
}

static void teardown(h3_io_t* io)
{
    CHECK(io);
    if (io->event_thread)
    {
        io->thread_running = 0;
        if (io->wakeup_pipe[1])
        {
            char wake = '1';
            apr_size_t len = 1;
            (void)apr_file_write(io->wakeup_pipe[1], &wake, &len);
        }
        apr_status_t status;
        apr_thread_join(&status, io->event_thread);
        io->event_thread = NULL;
    }
    if (io->h3_worker_pool)
    {
        apr_thread_pool_destroy(io->h3_worker_pool);
        io->h3_worker_pool = NULL;
    }
    if (io->active_sessions)
    {
        apr_time_t next_warning = apr_time_now() + apr_time_from_sec(5);
        while (io->active_sessions->nelts > 0)
        {
            if (apr_time_now() >= next_warning)
            {
                ap_log_error(APLOG_MARK, APLOG_WARNING, 0, io->server, "teardown waiting for %d connections to finish in-flight streams", io->active_sessions->nelts);
                next_warning = apr_time_now() + apr_time_from_sec(5);
            }
            apr_sleep(50 * 1000);
        }
    }
    if (io->qengine)
    {
        quic_engine_destroy(io->qengine);
        io->qengine = NULL;
    }
    if (io->udp_fd >= 0)
    {
        h3_socket_close(io->udp_fd);
        io->udp_fd = -1;
    }
    if (io->wakeup_pipe[0])
    {
        apr_file_close(io->wakeup_pipe[0]);
        apr_file_close(io->wakeup_pipe[1]);
        io->wakeup_pipe[0] = NULL;
        io->wakeup_pipe[1] = NULL;
    }
}

apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_conf* conf, int udp_fd)
{
    CHECK(pchild);
    CHECK(s);
    CHECK(conf);
    h3_io_t* io = apr_pcalloc(pchild, sizeof(*io));
    io->pool = pchild;
    io->server = s;
    io->udp_fd = udp_fd;
    io->active_sessions = apr_array_make(pchild, 8, sizeof(h3_session*));
    io->pending_handshakes = apr_array_make(pchild, 4, sizeof(h3_pending_handshake));
    if (apr_file_pipe_create_ex(&io->wakeup_pipe[0], &io->wakeup_pipe[1], APR_FULL_NONBLOCK, pchild) != APR_SUCCESS)
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "apr_file_pipe_create_ex failed");
        return APR_EGENERAL;
    }
    if (apr_thread_pool_create(&io->h3_worker_pool, 16, 64, pchild) != APR_SUCCESS)
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "apr_thread_pool_create failed");
        return APR_EGENERAL;
    }

    char qerr[QUIC_ERRLEN] = {0};
    quic_config qcfg = {
        .cred = {.kind = QUIC_CRED_FILE, .as.file = {.cert_path = conf->h3_cert_path, .key_path = conf->h3_key_path}},
        .callbacks = {.stream_acked = h3_session_on_stream_acked},
        .io = &io->qio,
    };
    quic_settings_default(&qcfg.settings);
    qcfg.settings.max_idle_timeout_ms = (uint64_t)conf->h3_idle_timeout * 1000;
    qcfg.settings.address_validation = (conf->h3_address_validation != H3_FLAG_OFF);
    quic_io_udp_init(&io->qio, udp_fd);
    io->qengine = quic_engine_create(&qcfg, qerr, sizeof(qerr));
    if (!io->qengine)
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "QUIC engine initialization failed: %s", qerr);
        teardown(io);
        return APR_EGENERAL;
    }

    io->note_conn_added = APR_RETRIEVE_OPTIONAL_FN(ap_mpm_note_extra_connection_added);
    io->note_conn_removed = APR_RETRIEVE_OPTIONAL_FN(ap_mpm_note_extra_connection_removed);
    if (!io->note_conn_added || !io->note_conn_removed)
    {
        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, "active MPM '%s' lacks connection-count notifications; upgrade your httpd to a version that supports mod_http3", ap_show_mpm());
        teardown(io);
        return APR_EGENERAL;
    }

    io->thread_running = 1;
    child_h3_io = io;
    apr_threadattr_t* attr = NULL;
    if (apr_threadattr_create(&attr, pchild) != APR_SUCCESS || apr_thread_create(&io->event_thread, attr, quic_event_thread, io, pchild) != APR_SUCCESS)
    {
        io->thread_running = 0;
        teardown(io);
        child_h3_io = NULL;
        return APR_EGENERAL;
    }
    ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "mod_http3 loaded with version: %d (%s) on pid=%d port=%d", MOD_HTTP3_VERSION, MOD_HTTP3_VERSION_STRING, getpid(), (int)conf->h3_port);
    return APR_SUCCESS;
}

void h3_io_listen_stop(h3_io_t* io)
{
    if (io)
    {
        teardown(io);
        if (child_h3_io == io)
        {
            child_h3_io = NULL;
        }
    }
}

void wait_for_event(h3_io_t* io)
{
    if (!io)
    {
        return;
    }
    int want_read = 0;
    int want_write = 0;
    int timeout_ms = 1000;
    quic_engine_want(io->qengine, &want_read, &want_write, &timeout_ms);

    struct pollfd pfds[2] = {{.fd = io->udp_fd, .events = 0}, {.fd = -1, .events = POLLIN}};
    nfds_t npfds = 1;
    if (io->wakeup_pipe[0])
    {
        apr_os_file_t wakeup_fd = -1;
        apr_os_file_get(&wakeup_fd, io->wakeup_pipe[0]);
        pfds[1].fd = wakeup_fd;
        npfds = 2;
    }

    if (want_read)
    {
        pfds[0].events |= POLLIN;
    }
    if (want_write)
    {
        pfds[0].events |= POLLOUT;
    }
    if (!pfds[0].events)
    {
        pfds[0].events = POLLIN;
    }

    if (poll(pfds, npfds, timeout_ms) < 0 && errno == EINTR)
    {
        return;
    }

    if (npfds == 2 && (pfds[1].revents & POLLIN))
    {
        char buf[64];
        apr_size_t len = sizeof(buf);
        (void)apr_file_read(io->wakeup_pipe[0], buf, &len);
    }
}

void remove_pending_handshake(h3_io_t* io, int index, int free_conn)
{
    h3_pending_handshake* pending = (h3_pending_handshake*)io->pending_handshakes->elts;
    if (free_conn)
    {
        quic_conn_free(pending[index].conn);
    }
    if (index < io->pending_handshakes->nelts - 1)
    {
        pending[index] = pending[io->pending_handshakes->nelts - 1];
    }
    io->pending_handshakes->nelts--;
}

static apr_status_t spawn_serviced_session(h3_io_t* io, quic_conn* conn)
{
    apr_allocator_t* allocator = NULL;
    apr_pool_t* session_pool = NULL;
    if (apr_allocator_create(&allocator) != APR_SUCCESS || apr_pool_create_ex(&session_pool, io->pool, NULL, allocator) != APR_SUCCESS)
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "failed to create session pool for new connection");
        if (allocator)
        {
            apr_allocator_destroy(allocator);
        }
        quic_conn_free(conn);
        return APR_EGENERAL;
    }
    apr_allocator_owner_set(allocator, session_pool);
    apr_pool_tag(session_pool, "h3_session");

    h3_session* session = NULL;
    if (h3_session_create(&session, io->server, conn, session_pool) != APR_SUCCESS)
    {
        quic_conn_free(conn);
        apr_pool_destroy(session_pool);
        return APR_EGENERAL;
    }
    quic_conn_set_user(conn, session);
    if (h3_session_create_control_streams(session) != APR_SUCCESS)
    {
        h3_session_destroy(session);
        return APR_EGENERAL;
    }
    if (quic_conn_is_closed(conn))
    {
        h3_session_destroy(session);
        return APR_EGENERAL;
    }
    conn_rec* c = h3_synth_conn(session);
    if (!c)
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "h3_synth_conn failed");
        h3_session_destroy(session);
        return APR_EGENERAL;
    }
    *(h3_session**)apr_array_push(io->active_sessions) = session;
    apr_atomic_inc32(&io->active_session_count);
    if (io->note_conn_added)
    {
        io->note_conn_added();
    }
    return APR_SUCCESS;
}

void progress_pending_handshakes(h3_io_t* io)
{
    CHECK(io);
    h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
    CHECK(conf);
    apr_time_t now = apr_time_now();
    apr_time_t timeout = apr_time_from_sec(conf->h3_handshake_timeout);

    for (int i = 0; i < io->pending_handshakes->nelts;)
    {
        h3_pending_handshake* pending = &((h3_pending_handshake*)io->pending_handshakes->elts)[i];
        quic_conn* conn = pending->conn;

        if (now - pending->accepted_at >= timeout)
        {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "QUIC handshake timed out after %u second(s)", (unsigned)conf->h3_handshake_timeout);
            remove_pending_handshake(io, i, 1);
            continue;
        }

        int finished = 0;
        int rv = 0;
        const char* why = NULL;

        if (quic_conn_is_closed(conn))
        {
            rv = -1;
            why = "peer closed the connection during the handshake";
        }
        else if (quic_conn_is_handshake_done(conn))
        {
            rv = 1;
        }

        if (rv == 1)
        {
            ap_log_error(APLOG_MARK, APLOG_INFO, 0, io->server, "QUIC handshake complete");
            spawn_serviced_session(io, conn);
            remove_pending_handshake(io, i, 0);
            finished = 1;
        }
        else if (rv == -1)
        {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "QUIC handshake did not complete: %s", why);
            remove_pending_handshake(io, i, 1);
            finished = 1;
        }

        if (!finished)
        {
            i++;
        }
    }
}

int prepare_accepted_connection(h3_io_t* io, quic_conn* conn)
{
    h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
    if (!quic_conn_prepare(conn, conf->h3_idle_timeout))
    {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "quic_conn_prepare failed for accepted connection - dropping it");
        return 0;
    }

    h3_pending_handshake* pending = (h3_pending_handshake*)apr_array_push(io->pending_handshakes);
    pending->conn = conn;
    pending->accepted_at = apr_time_now();
    apr_atomic_inc32(&io->total_connections);
    return 1;
}

int service_session_pass(h3_io_t* io, h3_session* session)
{
    CHECK(io);
    CHECK(session);
    server_rec* s = session->s;
    quic_conn* conn = session->qconn;

    if (session->aborted)
    {
        return 0;
    }

    if (!io->thread_running)
    {
        apr_thread_mutex_lock(session->lock);
        if (!session->goaway_deadline)
        {
            nghttp3_conn_submit_shutdown_notice(session->ngh3);
        }
        flush_nghttp3(session);
        int drained = nghttp3_conn_is_drained2(session->ngh3);
        apr_thread_mutex_unlock(session->lock);
        if (!session->goaway_deadline)
        {
            session->goaway_deadline = apr_time_now() + apr_time_from_sec(H3_GOAWAY_GRACE_SECS);
            ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "sent HTTP/3 GOAWAY; allowing up to %d more second(s) for in-flight streams", H3_GOAWAY_GRACE_SECS);
        }
        if (drained || apr_time_now() >= session->goaway_deadline)
        {
            apr_thread_mutex_lock(session->lock);
            nghttp3_conn_shutdown(session->ngh3);
            flush_nghttp3(session);
            apr_thread_mutex_unlock(session->lock);
            session->aborted = 1;
            return 0;
        }
    }

    if (quic_conn_is_closed(conn))
    {
        ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "QUIC connection terminated (idle timeout, peer close, or transport error)");
        session->aborted = 1;
        return 0;
    }

    apr_pool_t* scratch = NULL;
    if (apr_pool_create(&scratch, session->pool) != APR_SUCCESS)
    {
        return 0;
    }

    for (quic_stream* s2 = NULL; (s2 = quic_conn_accept_stream(conn)) != NULL;)
    {
        apr_atomic_inc32(&io->total_streams);
        int64_t sid = quic_stream_id(s2);
        if (sid < 0)
        {
            quic_stream_free(s2);
            continue;
        }
        apr_thread_mutex_lock(session->lock);
        h3_stream* tracked = track_stream(session, sid, s2);
        if (!tracked)
        {
            ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "track_stream failed for sid=%lld - freeing stream", (long long)sid);
            quic_stream_free(s2);
        }
        apr_thread_mutex_unlock(session->lock);
    }

    int data_read = 0;
    apr_thread_mutex_lock(session->lock);
    apr_array_header_t* completed = drain_ready_streams(session, scratch, &data_read);
    flush_nghttp3(session);
    apr_thread_mutex_unlock(session->lock);

    if (session->ngh3_dead)
    {
        session->aborted = 1;
        apr_pool_destroy(scratch);
        return 0;
    }

    for (int i = 0; i < completed->nelts; i++)
    {
        h3_stream* h3s = ((h3_stream**)completed->elts)[i];
        h3_process_request(session, h3s);
    }

    apr_thread_mutex_lock(session->lock);
    flush_nghttp3(session);
    apr_thread_mutex_unlock(session->lock);
    apr_pool_destroy(scratch);
    return data_read;
}