Skip to content

HTTP2 Guide

github-actions[bot] edited this page Jul 21, 2026 · 13 revisions

HTTP/2 Guide

Elio provides full HTTP/2 client support via the nghttp2 library. This guide covers HTTP/2 usage, configuration, and best practices.

Overview

HTTP/2 offers several advantages over HTTP/1.1:

  • Multiplexing: Multiple requests/responses over a single connection
  • Header compression: HPACK compression reduces overhead
  • Binary framing: More efficient parsing
  • Stream prioritization: Clients can hint at request importance

Why nghttp2

Elio uses the nghttp2 library for HTTP/2 support. nghttp2 is the reference implementation of the HTTP/2 protocol (RFC 7540) and handles the substantial complexity of HTTP/2 streams, flow control, and HPACK header compression. It is well-tested and widely deployed -- used by curl, Apache httpd, and many other projects. Elio wraps nghttp2 to expose a simple coroutine-based interface, letting the library manage the protocol state machine while Elio handles I/O scheduling and connection lifecycle.

Requirements

HTTP/2 support requires:

  • OpenSSL with ALPN support
  • nghttp2 library
  • CMake option: -DELIO_ENABLE_HTTP2=ON

Note: HTTP/2 requires HTTPS (h2 over TLS). For plaintext HTTP, use the HTTP/1.1 client.

When building Elio from source with FetchContent or add_subdirectory, the repository CMake configuration can fetch and build nghttp2 for the elio_http2 target. Installed-package consumers do not fetch nghttp2 during find_package(Elio): the installed package must contain the bundled nghttp2 export, or CMake must be able to find a system nghttp2 library and nghttp2/nghttp2.h through normal package-manager installs, CMAKE_PREFIX_PATH, or equivalent CMake hints.

Link the HTTP/2 feature target so nghttp2 headers and libraries propagate to your application:

# FetchContent or add_subdirectory
set(ELIO_ENABLE_TLS ON CACHE BOOL "" FORCE)
set(ELIO_ENABLE_HTTP ON CACHE BOOL "" FORCE)
set(ELIO_ENABLE_HTTP2 ON CACHE BOOL "" FORCE)

# The options must be set before either of these calls:
# FetchContent_MakeAvailable(elio)
# add_subdirectory(path/to/elio)
target_link_libraries(your_target PRIVATE elio_http2)

# Installed package built with TLS, HTTP, and HTTP/2 enabled
# find_package(Elio REQUIRED CONFIG)
target_link_libraries(your_target PRIVATE Elio::elio_http2)

Basic Usage

Simple GET Request

#include <elio/elio.hpp>
#include <elio/http/http2.hpp>

using namespace elio;
using namespace elio::http;

coro::task<void> fetch_example() {
    h2_client client;

    // Simple GET request
    auto resp = co_await client.get("https://example.com/");
    if (resp) {
        std::cout << "Status: " << resp->status_code() << std::endl;
        std::cout << "Body: " << resp->body() << std::endl;
    }
}

POST Request with JSON

coro::task<void> post_example() {
    h2_client client;

    auto resp = co_await client.post(
        "https://api.example.com/users",
        R"({"name": "John", "email": "john@example.com"})",
        mime::application_json
    );

    if (resp) {
        std::cout << "Created: " << resp->body() << std::endl;
    }
}

Client Configuration

h2_client_config

struct h2_client_config {
    std::chrono::seconds connect_timeout{10};  // TCP connect + TLS handshake timeout
    std::chrono::seconds read_timeout{30};     // Session I/O timeout; <=0 disables
    size_t max_concurrent_streams = 100;
    uint32_t initial_window_size = 65535;
    size_t max_response_size = 16 * 1024 * 1024;  // Max buffered body bytes
    std::string user_agent = "elio-http2/1.0";
    bool enable_push = false;  // Advertise SETTINGS_ENABLE_PUSH only;
                               // pushed responses are not exposed
    net::resolve_options resolve_options = net::default_cached_resolve_options();
    bool rotate_resolved_addresses = true;
    size_t max_response_headers = 100;             // Max accepted field lines per stream
    size_t max_response_header_bytes = 64 * 1024; // Max accepted name/value bytes per stream
};

// Usage
h2_client_config config;
config.max_concurrent_streams = 50;

h2_client client(config);

connect_timeout bounds TCP connect and TLS/ALPN handshake setup. read_timeout bounds HTTP/2 session initialization and response waits. max_response_size bounds the accumulated DATA payload stored in the returned http::response; responses exceeding the limit fail instead of continuing to buffer in memory. max_response_headers and max_response_header_bytes bound accepted regular response field lines per stream. Informational response fields are discarded and reset the live budget when the next response block begins; final response headers and trailers share one budget. Exceeding either limit rejects the field, resets only that stream with ENHANCE_YOUR_CALM, and makes the request fail with errno == EMSGSIZE. enable_push only controls the HTTP/2 SETTINGS_ENABLE_PUSH value sent to the peer. Elio does not currently expose pushed responses through the public client API, so applications cannot observe or consume server-pushed streams.

TLS Configuration

h2_client client;

// Access the underlying TLS context
auto& tls_ctx = client.tls_context();

// Use custom CA certificate
tls_ctx.load_verify_locations("/path/to/ca-bundle.crt");

// Use client certificate (for mutual TLS)
tls_ctx.load_certificate("/path/to/client.crt");
tls_ctx.load_private_key("/path/to/client.key");

Connection Reuse and Concurrency

h2_client reuses pooled HTTP/2 connections for sequential requests. The current high-level client does not coordinate multiple in-flight requests over one shared connection; callers that need application-level concurrency should use separate client instances or serialize calls until shared-session multiplexing is implemented.

coro::task<void> repeated_requests() {
    h2_client client;

    for (int id = 1; id <= 3; ++id) {
        auto resp = co_await client.get("https://api.example.com/users/" +
                                        std::to_string(id));
        if (!resp) {
            co_return;
        }
    }
}

Response Handling

coro::task<void> handle_response() {
    h2_client client;

    auto resp = co_await client.get("https://example.com/api/data");

    if (!resp) {
        std::cerr << "Request failed: " << strerror(errno) << std::endl;
        co_return;
    }

    // Check status
    if (!resp->is_success()) {
        std::cerr << "HTTP error: " << resp->status_code() << std::endl;
        co_return;
    }

    // Access headers
    auto content_type = resp->header("Content-Type");
    auto cache_control = resp->header("Cache-Control");

    // Get body (returns string_view)
    std::string_view body = resp->body();

    // If you need an owned copy:
    std::string body_owned(resp->body());
}

Error Handling

coro::task<void> error_handling() {
    h2_client client;

    auto resp = co_await client.get("https://example.com/");

    if (!resp) {
        switch (errno) {
            case EOPNOTSUPP:
                std::cerr << "Unsupported HTTP/2 request" << std::endl;
                break;
            case ECONNREFUSED:
                std::cerr << "Connection refused" << std::endl;
                break;
            case ETIMEDOUT:
                std::cerr << "Connection timeout" << std::endl;
                break;
            case ECONNRESET:
                std::cerr << "Connection reset" << std::endl;
                break;
            default:
                std::cerr << "Error: " << strerror(errno) << std::endl;
        }
        co_return;
    }

    // Handle HTTP-level errors
    if (resp->status_code() >= 400) {
        std::cerr << "HTTP " << resp->status_code() << ": "
                  << resp->body() << std::endl;
    }
}

h2_client::send() rejects method::CONNECT with EOPNOTSUPP. The current HTTP/2 client supports ordinary request/response methods over TLS, but it does not implement HTTP/2 tunneling.

Cancellation

coro::task<void> cancellable_request() {
    h2_client client;

    // Note: h2_client does not currently support per-request cancellation
    // via cancel_token. Use task-level cancellation instead.
    auto resp = co_await client.get("https://slow-api.example.com/");

    if (!resp) {
        std::cout << "Request failed" << std::endl;
    }
}

Connection Management

The h2_client maintains a connection pool internally:

coro::task<void> connection_lifecycle() {
    h2_client client;

    // First request establishes connection
    co_await client.get("https://api.example.com/ping");

    // Subsequent requests reuse the same connection
    for (int i = 0; i < 100; i++) {
        co_await client.get("https://api.example.com/data/" +
                           std::to_string(i));
    }

    // Connection is automatically closed when client is destroyed
}

HTTP/2 vs HTTP/1.1

Feature HTTP/2 HTTP/1.1
Multiplexing Protocol/session support; high-level client serializes pooled use No (pipelining limited)
Header compression HPACK None
Binary protocol Yes Text-based
Server push SETTINGS advertisement only; pushed responses are not exposed No
TLS required Yes (in practice) No
Connection reuse Sequential pooled reuse Connection pool

When to Use HTTP/2

Use HTTP/2 when:

  • Making repeated HTTPS requests to the same host
  • Bandwidth is limited (header compression helps)
  • Low latency is critical
  • Server supports HTTP/2

Use HTTP/1.1 when:

  • Server doesn't support HTTP/2
  • Making single requests
  • HTTP (non-TLS) is required
  • Compatibility with older infrastructure

Debugging

Enable debug logging to see HTTP/2 frame details:

// Set log level before creating client
elio::log::logger::instance().set_level(elio::log::level::debug);

h2_client client;
auto resp = co_await client.get("https://example.com/");

// Output shows frame exchanges:
// [DEBUG] Sending SETTINGS frame
// [DEBUG] Received SETTINGS frame
// [DEBUG] Sending HEADERS frame (stream 1)
// [DEBUG] Received HEADERS frame (stream 1)
// [DEBUG] Received DATA frame (stream 1, 1234 bytes)

Best Practices

  1. Reuse clients: Create one h2_client per host and reuse it for sequential requests
  2. Avoid concurrent use of one h2_client: use separate clients for parallel work until high-level shared-session multiplexing is implemented
  3. Handle errors: Always check response validity
  4. Set timeouts: Configure appropriate timeouts for your use case
  5. Verify certificates: Only disable certificate verification for testing

See Also

Clone this wiki locally