TLS Configuration Guide ​
HTTPX implements Transport Layer Security with full cross-platform support across Linux, Windows, and macOS.
Support Scope (read this first) ​
- Client (
httpx.Client→https://): HTTPS/1.1 without client certificates runs onstd.crypto.tls(TLS 1.2/1.3) with SNI, system + custom trust stores, and hostname verification. Setting.tls = .{ .clientCertPem, .clientKeyPem }(mTLS) or explicit.httpVersion = .http2switches that request to the native client instead, adding certificate presentation and/or ALPN (h2for HTTP/2,http/1.1otherwise) on top of the same verification. - Server (native engine in
src/protocols/tls/): TLS 1.3 only, with X25519 ECDHE, AES-128-GCM / AES-256-GCM / ChaCha20-Poly1305 record protection, SNI parsing, and ALPN dispatch. Server certificates must be P-256 ECDSA (ecdsa_secp256r1_sha256); RSA/private-key types are rejected loudly withUnsupportedSignatureSchemeinstead of emitting a broken handshake. - Session resumption (TLS 1.3 PSK,
psk_dhe_ke): the native client offers cached sessions and the native server issues stateless tickets — see Session Resumption below. Resumption covers the native paths (HTTP/2 over TLS always; HTTPS/1.1 when already native via client certificates). Plain HTTPS/1.1 runs onstd.crypto.tls, which exposes no ticket API, so it always does full handshakes. - HelloRetryRequest: fully handled on both sides (shareless hello → retry with share, single-retry guard, transcript splice). Only X25519 is supported: a server selecting any other group fails loudly.
- 0-RTT Early Data (RFC 8446 & RFC 9001): Fully supported across TLS 1.3, QUIC TLS, and HTTP/3. Safe by default: early data is disabled unless explicitly enabled via
.earlyData = .{ .enabled = true }. Safe HTTP methods (GET,HEAD,OPTIONS) are permitted by default; replay-sensitive methods (POST,PUT,PATCH,DELETE) require explicit.allowUnsafeMethods = true. Servers enforce bounded anti-replay defense viaReplayCache, ticket age checks, andmaxEarlyDatabounds.
Mutual TLS (Client Certificates) ​
The server can require (or optionally accept) client certificates:
var server = try httpx.Server.init(allocator, io, .{
.host = "127.0.0.1",
.port = 0,
.tls = .{
.certPem = cert_pem,
.keyPem = key_pem,
.clientAuth = .required, // or .optional / .disabled (default)
.clientCa = ca_pem, // PEM bundle trusted for client chains
},
});With .required, a missing certificate fails the handshake: the client observes error.ClientCertificateRequired (its native handshake refuses to continue cert-less) while the server side only ever sees the connection vanish mid-flight (error.IoError / error.TlsHandshakeFailed) — there is no server-side policy error to assert on. Presented chains must anchor in clientCa with valid signatures (error.ClientCertificateInvalid otherwise). .optional lets cert-less clients through while still verifying any presented chain. See [TLS mTLS](/examples/tls-mtls) for a runnable loopback demo.
A client presents its certificate through the high-level API — no engine calls needed:
var res = try client.get("https://service.internal/", .{
.tls = .{
.verify = .caBundle,
.caPem = ca_pem, // extra trust anchor for the server chain
.clientCertPem = cert_pem, // presented when requested
.clientKeyPem = key_pem, // P-256 ECDSA key for the chain
},
});
defer res.deinit();Omitting the pair on a .required server fails the request loudly; a non-http/1.1 ALPN answer on the HTTP/1.x path (and anything but h2 on the .http2 path) fails with error.AlpnNegotiationFailed instead of silently downgrading.
Session Resumption (PSK / Tickets) ​
TLS 1.3 resumption (RFC 8446 Sections 4.6.1, 4.2.11) abbreviates repeat handshakes: no Certificate/CertificateVerify flight, authentication via the PSK binder, forward secrecy preserved (psk_dhe_ke always performs fresh ECDHE alongside the PSK).
Server — opt in with ticket keys (stateless; no per-client storage):
var server = try httpx.Server.init(allocator, io, .{
.port = 8443,
.tls = .{
.certPem = cert_pem,
.keyPem = key_pem,
.ticketKeys = httpx.tls.TicketKeys.generate(),
.ticketLifetimeSecs = 7200,
},
});Rotate with keys.rotate(next); outstanding tickets stay valid through one rotation via the previous-key slot, then fail closed (clients fall back to full handshakes — never an alert storm).
Client — automatic on the native paths: httpx.Client keeps an origin-keyed session cache, offers usable tickets, captures new ones from NewSessionTicket messages during reads, and resumes transparently. No API changes needed.
Rules that keep resumption honest:
- Any ticket problem (unknown/expired/corrupt ticket, binder mismatch, suite mismatch) silently falls back to a full handshake — the client cannot distinguish either way.
- Resumption is disabled under mutual TLS: an abbreviated flight carries no
CertificateRequest, so resumed connections would bypass client certificate authentication. Servers withclientAuthset always do full handshakes. - Only SHA-256 suites resume (
AES_128_GCM_SHA256,CHACHA20_POLY1305_SHA256); tickets for other hashes are ignored. - Tickets bind to the issuing origin host; the client never offers a ticket to a different host.
Client HTTPS Usage ​
HTTPS works out of the box with zero configuration:
var client = httpx.Client.init(allocator, io, .{});
defer client.deinit();
// Automatically performs TLS 1.3 handshake with SNI and ALPN
const response = try client.get("https://cloudflare.com", .{});
defer response.deinit();Custom CA Bundles & Self-Signed Certs ​
For local development or internal PKI:
const response = try client.get("https://internal.corp", .{
.tls = .{
.verify = .selfSigned, // Accept self-signed certificates
.allowTruncation = true,
},
});
defer response.deinit();Server TLS Configuration ​
HTTPX provides first-class native TLS/HTTPS support directly integrated into httpx.Server.
To launch a secure HTTPS server, supply PEM-encoded certificate chain and private key (either as in-memory PEM string or as file path):
const certPem = @embedFile("certs/server.crt");
const keyPem = @embedFile("certs/server.key");
var server = try httpx.Server.init(allocator, io, .{
.port = 8443,
.tls = .{
.certPem = certPem,
.keyPem = keyPem,
.allowPlainHttp = false, // strict HTTPS mode (default)
},
});
defer server.deinit();
try server.get("/", helloHandler);
server.run();Handlers can inspect the connection's encryption status via Context (ctx.isTls, ctx.scheme()), as in helloHandler below:
fn helloHandler(ctx: *httpx.Context) anyerror!httpx.Response {
if (ctx.isTls) return ctx.text("Hello HTTPS!");
return ctx.text("Hello HTTP!");
}Dynamic TLS Reconfiguration ​
You can inspect and reconfigure TLS at runtime without stopping the server:
// Check if TLS is currently active
if (server.isTls()) {
std.debug.print("Server running in HTTPS mode\n", .{});
}
// Dynamically rotate or enable certificates and keys
try server.setTls("certs/new_cert.pem", "certs/new_key.pem");When rotating or deinitializing certificates, private key memory in heap buffers is automatically zeroed using std.crypto.secureZero before release.
Handshake Detection & Strict HTTPS Rejection ​
When TLS is active on the server port, HTTPX peeks at the initial connection bytes:
- If the bytes start with the TLS record header (
0x16 0x03), HTTPX executes the TLS server handshake. - If a plain HTTP request (e.g.
GET / HTTP/1.1) arrives on an HTTPS port andallowPlainHttpisfalse(the default), HTTPX immediately rejects the connection with:httpHTTP/1.1 400 Bad Request Content-Type: text/plain Connection: close The plain HTTP request was sent to HTTPS port - If
allowPlainHttp = true, HTTPX seamlessly routes cleartext HTTP requests on the same port (dual HTTP/HTTPS mode, ideal for local testing).
ALPN Protocol Negotiation ​
During the TLS handshake, HTTPX negotiates the application protocol via ALPN:
h2: Dispatched to HTTP/2 binary multiplexed connection handler.http/1.1: Dispatched to HTTP/1.1 connection pipeline.
Connection Security & Context ​
Handlers can inspect the connection's encryption status via Context:
ctx.isTls:boolindicating whether the request was received over TLS.ctx.scheme(): Returns"https"for TLS connections (or if trustedX-Forwarded-Protoindicates HTTPS).
Structured Event Logging ​
When a client fails TLS handshakes (malformed ClientHello, unsupported ciphers, or aborted handshake), HTTPX emits a structured non-allocating event:
event.kind == .tlsHandshakeFailedThis allows application observability without emitting unauthorized stdout/stderr noise.
Error Taxonomy ​
When certificate verification or handshake fails, HTTPX returns precise error types:
error.CertificateExpired: Certificate validity window has passed.error.CertificateHostMismatch: Certificate SAN/CN does not match requested host.error.CertificateIssuerMismatch: Intermediate CA signature could not be verified.error.TlsCertificateNotVerified: Certificate not trusted by root CA store.error.TlsHandshakeFailed: General TLS protocol or alert failure.
