-
Notifications
You must be signed in to change notification settings - Fork 0
HTTP2 Guide
Elio provides full HTTP/2 client support via the nghttp2 library. This guide covers HTTP/2 usage, configuration, and best practices.
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
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.
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)#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;
}
}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;
}
}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.
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");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;
}
}
}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());
}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.
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;
}
}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
}| 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 |
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
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)- Reuse clients: Create one h2_client per host and reuse it for sequential requests
- Avoid concurrent use of one h2_client: use separate clients for parallel work until high-level shared-session multiplexing is implemented
- Handle errors: Always check response validity
- Set timeouts: Configure appropriate timeouts for your use case
- Verify certificates: Only disable certificate verification for testing