TLS & transport

HTTP/2 runs over two flavours of transport: h2 (TLS-wrapped, the default on the public internet per RFC 9113 §3.3) and h2c (HTTP/2 over cleartext TCP, RFC 9113 §3.4). At Milestone 5, PureHTTP2.jl is server-role only and delivers h2c end-to-end over any Julia Base.IO transport. Client-role code and live server-side TLS ALPN are out of scope at M5 — see the limitations section at the bottom of this page.

h2c vs h2

ProtocolTransportNegotiationPureHTTP2.jl status
h2ccleartext TCPKnown at connect time (RFC 9113 §3.4, client magic PRI * HTTP/2.0)✅ fully supported at M5 via serve_connection!
h2TLS ≥ 1.2TLS ALPN (RFC 7301)⚠️ client-side ALPN helper scaffolded, server-side deferred (see below)

h2c is the natural fit for gRPC over private networks, CI harnesses, and cross-tests against reference implementations like nghttp2. h2 is required by browsers.

IO adapter contract

serve_connection! drives an HTTP2Connection through any Base.IO value that implements three methods:

MethodSemantics
read(io, n::Int) :: Vector{UInt8}Read up to n bytes. May return fewer than n only on EOF — a partial return followed by EOF is treated as a graceful close and the read loop exits.
write(io, bytes)Write all bytes. Standard Base.write contract.
close(io)Terminate the transport. The caller of serve_connection! is responsible for closing io once the function returns.

eof(io) is optional. If your transport type supports it, the loop will still work; if it does not, the read short-return is the EOF signal.

The contract deliberately does not require thread safety, backpressure, timeouts, or isopen(io) — these are added by the caller when needed. See specs/006-tls-alpn-support/contracts/README.md in the repository for the formal contract and the list of PR-gated "how to break it" rules.

Transports known to satisfy the contract

TransportUse case
Base.IOBufferIn-memory unit tests (needs a split-IO wrapper for bidirectional use)
Base.BufferStreamPaired in-memory pipes (testing blocking reads)
Base.PipeProcess-boundary I/O
Sockets.TCPSocketReal h2c over loopback or production TCP
OpenSSL.SSLStreamForward-compat with h2 (not live-tested at M5)
PureHTTP2.serve_connection!Function
serve_connection!(conn::HTTP2Connection, io::IO; max_frame_size::Int = DEFAULT_MAX_FRAME_SIZE) -> Nothing

Drive an HTTP2Connection over an arbitrary Base.IO transport.

This is PureHTTP2.jl's primary server-side entry point for real traffic. The function:

  1. Reads the 24-byte client connection preface and validates it via process_preface. On short read or invalid preface, throws ConnectionError with PROTOCOL_ERROR.
  2. Writes the server preface (SETTINGS frame) to io.
  3. Enters a read loop: read a 9-byte frame header via decode_frame_header, enforce header.length ≤ max_frame_size (else throws ConnectionError with FRAME_SIZE_ERROR), read the payload, dispatch via process_frame, and write any response frames back to io.
  4. Exits cleanly when the transport reports EOF (read returns fewer bytes than requested) or when the connection enters the CLOSED state (e.g., after a GOAWAY with a non-zero error code).

The caller owns io and is responsible for closing it after this function returns.

max_frame_size defaults to DEFAULT_MAX_FRAME_SIZE (16 KiB, the RFC 9113 §6.5.2 default). Peers may negotiate a larger value via SETTINGS; pass the negotiated value if known.

Transport contract

See specs/006-tls-alpn-support/contracts/README.md for the full contract. The minimum: Base.read(io, n::Int), Base.write(io, bytes), Base.close(io).

Example

using PureHTTP2, Sockets

server = listen(8080)
while true
    sock = accept(server)
    @async begin
        conn = HTTP2Connection()
        try
            serve_connection!(conn, sock)
        finally
            close(sock)
        end
    end
end
source

Driving PureHTTP2.jl over a raw socket

The canonical h2c server loop on real TCP:

using PureHTTP2, Sockets

server = listen(IPv4(0x7f000001), 8080)  # 127.0.0.1:8080
while isopen(server)
    sock = accept(server)
    @async begin
        conn = HTTP2Connection()
        try
            serve_connection!(conn, sock)
        catch err
            @warn "h2c connection terminated" exception=err
        finally
            close(sock)
        end
    end
end

This loop has been cross-tested at M5 against the libnghttp2 reference implementation via Nghttp2Wrapper.jl — see the Interop: h2c live TCP handshake test item in test/interop/testitems_interop.jl.

TLS backends

PureHTTP2.jl does not depend on any TLS library at runtime. Its [deps] block is empty by design (constitution Principle I). Instead, PureHTTP2.jl ships two optional TLS backends as Julia package extensions. You opt into whichever one your environment already uses; PureHTTP2.jl itself is agnostic and accepts any Base.IO satisfying the IO adapter contract.

BackendExtension moduleClient ALPNServer ALPNUse when
OpenSSL.jlPureHTTP2OpenSSLExt✅ via set_alpn_h2!❌ blocked on upstream bindingYou're already depending on OpenSSL.jl or want a mutable SSLContext you can configure piecemeal.
Reseau.jlPureHTTP2ReseauExt✅ via reseau_h2_client_config / reseau_h2_connect✅ via reseau_h2_server_configYou need server-side h2 over TLS today, or you already depend on Reseau.jl for other reasons.

Both extensions coexist — loading both packages activates both sets of helpers simultaneously. There are no method collisions: the two backends use different generic function names (set_alpn_h2! for OpenSSL, reseau_h2_* for Reseau).

OpenSSL.jl

When using OpenSSL is in scope alongside using PureHTTP2, Julia's package-extension mechanism activates PureHTTP2OpenSSLExt, which adds one method to the generic set_alpn_h2! function:

using PureHTTP2, OpenSSL

ctx = OpenSSL.SSLContext(OpenSSL.TLSClientMethod())
PureHTTP2.set_alpn_h2!(ctx)                    # register "h2"
PureHTTP2.set_alpn_h2!(ctx, ["h2", "http/1.1"])  # with fallback

Under the hood the helper converts the Vector{String} into the RFC 7301 §3.1 wire format (length-prefixed concatenation, max 255 bytes per protocol) and calls OpenSSL.jl's ssl_set_alpn, which wraps SSL_CTX_set_alpn_protos. Names longer than 255 bytes are rejected with ArgumentError before any ccall.

PureHTTP2.set_alpn_h2!Function
set_alpn_h2!(ctx, protocols=["h2"])

Register the HTTP/2 ALPN protocol identifier on a TLS context.

This is a generic function whose methods are provided by the PureHTTP2OpenSSLExt package extension. The extension loads automatically via Base.get_extension when OpenSSL.jl is present in the same environment as PureHTTP2.jl.

Without OpenSSL.jl loaded, this function has zero methods and calling it throws MethodError — by design. PureHTTP2.jl's runtime dependency graph stays empty (constitution Principle I); OpenSSL is a weak dependency activated only when you using OpenSSL.

With OpenSSL.jl loaded, a method for OpenSSL.SSLContext becomes available:

using PureHTTP2, OpenSSL
ctx = OpenSSL.SSLContext(OpenSSL.TLSClientMethod())
PureHTTP2.set_alpn_h2!(ctx)              # register "h2"
PureHTTP2.set_alpn_h2!(ctx, ["h2", "http/1.1"])  # register with fallback

The method converts the user-facing Vector{String} into the RFC 7301 §3.1 wire format (length-prefixed concatenation) before handing off to OpenSSL.

Current limitations

At Milestone 5, PureHTTP2.jl is server-role only and OpenSSL.jl does not yet export SSL_CTX_set_alpn_select_cb (the server-side selection callback). set_alpn_h2! is therefore scaffolded for forward compatibility with Milestone 6's client-role work and is not yet live-tested end-to-end against a real TLS peer. See docs/src/tls.md for the full story and upstream-bugs.md for the upstream tracking entry.

source

OpenSSL.jl caveat: set_alpn_h2! is client-side only at Milestone 7.5 because OpenSSL.jl does not yet bind SSL_CTX_set_alpn_select_cb, the server-side selection callback required to negotiate h2 in a handshake initiated by a client. PureHTTP2.jl's upstream-bugs.md entry for this gap is marked worked-around via Reseau.jl (see the Reseau backend below) — users who specifically want the OpenSSL-only code path still need the upstream binding to land.

Reseau.jl

When using Reseau is in scope alongside using PureHTTP2, the PureHTTP2ReseauExt extension activates and adds three constructor-style helpers:

using PureHTTP2, Reseau

# Server side: hand to Reseau.TLS.listen
server_cfg = PureHTTP2.reseau_h2_server_config(;
    cert_file = "server.crt",
    key_file  = "server.key",
)

listener = Reseau.TLS.listen("tcp", "0.0.0.0:443", server_cfg)
conn = Reseau.TLS.accept(listener)
Reseau.TLS.handshake!(conn)
# Reseau.TLS.connection_state(conn).alpn_protocol is now "h2"
PureHTTP2.serve_connection!(PureHTTP2.HTTP2Connection(), conn)
using PureHTTP2, Reseau

# Client side: one-shot h2-over-TLS connect
client = PureHTTP2.reseau_h2_connect("tcp", "example.com:443";
    server_name = "example.com")

conn = HTTP2Connection()
result = PureHTTP2.open_connection!(conn, client;
    request_headers = Tuple{String,String}[
        (":method",    "GET"),
        (":path",      "/"),
        (":scheme",    "https"),
        (":authority", "example.com"),
    ])
close(client)
PureHTTP2.reseau_h2_server_configFunction
PureHTTP2.reseau_h2_server_config(; cert_file, key_file, kwargs...) -> Reseau.TLS.Config

Build a Reseau.jl server-side TLS config with alpn_protocols=["h2"] pre-populated. Requires cert_file::AbstractString and key_file::AbstractString as keyword arguments; forwards every other keyword argument to Reseau.TLS.Config. If the caller passes an explicit alpn_protocols= kwarg, that value overrides the default ALPN_H2_PROTOCOLS.

This generic function is a stub in the main module — a method for Reseau.TLS.Config is provided by the PureHTTP2ReseauExt package extension, which loads automatically when Reseau.jl is in the environment. Without Reseau loaded, calling this function throws MethodError.

Example

using PureHTTP2, Reseau

cfg = PureHTTP2.reseau_h2_server_config(;
    cert_file = "server.crt",
    key_file  = "server.key",
)

listener = Reseau.TLS.listen("tcp", "0.0.0.0:443", cfg)
conn = Reseau.TLS.accept(listener)
Reseau.TLS.handshake!(conn)
# At this point Reseau.TLS.connection_state(conn).alpn_protocol
# is "h2" (client advertised it) or nothing (client did not).
PureHTTP2.serve_connection!(PureHTTP2.HTTP2Connection(), conn)

Why not set_alpn_h2!?

Milestone 5 shipped PureHTTP2.set_alpn_h2!(ctx::OpenSSL.SSLContext) as a mutator on a mutable C-backed context. Reseau.TLS.Config is an immutable Julia struct whose alpn_protocols field is defensively copied at construction, so an analogous mutator is structurally impossible. The reseau_h2_* helpers are constructor-style instead. See specs/009-reseau-tls-backend/contracts/README.md Section 2 for the full symmetry-break rationale.

source
PureHTTP2.reseau_h2_client_configFunction
PureHTTP2.reseau_h2_client_config(; kwargs...) -> Reseau.TLS.Config

Build a Reseau.jl client-side TLS config with alpn_protocols=["h2"] pre-populated. Thin convenience wrapper around Reseau.TLS.Config — forwards all keyword arguments. If the caller passes an explicit alpn_protocols= kwarg, that value overrides the default ALPN_H2_PROTOCOLS.

This generic function is a stub in the main module — a method for the Reseau config type is provided by the PureHTTP2ReseauExt package extension, which loads automatically when Reseau.jl is in the environment. Without Reseau loaded, calling this function throws MethodError.

See also: reseau_h2_server_config, reseau_h2_connect, ALPN_H2_PROTOCOLS.

source
PureHTTP2.reseau_h2_connectFunction
PureHTTP2.reseau_h2_connect(address::AbstractString; kwargs...) -> Reseau.TLS.Conn

One-shot client helper: calls Reseau.TLS.connect(address; ...) with alpn_protocols=["h2"] merged into the keyword arguments. Returns a fully-handshaken Reseau.TLS.Conn ready to hand to open_connection!.

If the caller passes an explicit alpn_protocols= kwarg, that value overrides the default ALPN_H2_PROTOCOLS. Other Reseau.jl connect keywords such as server_name, verify_peer, and handshake_timeout_ns are forwarded unchanged.

This generic function is a stub in the main module — a method is provided by the PureHTTP2ReseauExt package extension, which loads automatically when Reseau.jl is in the environment. Without Reseau loaded, calling this function throws MethodError.

Example

using PureHTTP2, Reseau

client = PureHTTP2.reseau_h2_connect("tcp", "example.com:443";
    server_name = "example.com")

conn = HTTP2Connection()
result = PureHTTP2.open_connection!(conn, client;
    request_headers = Tuple{String,String}[
        (":method",    "GET"),
        (":path",      "/"),
        (":scheme",    "https"),
        (":authority", "example.com"),
    ])

close(client)
source
PureHTTP2.ALPN_H2_PROTOCOLSConstant
PureHTTP2.ALPN_H2_PROTOCOLS :: Vector{String}

The canonical ALPN protocol list for HTTP/2 (["h2"], per RFC 7301 §3.1 and RFC 9113 §3.3). Reusable by any TLS backend — PureHTTP2.jl ships two optional TLS backend extensions that both consume this list:

Callers should treat this constant as read-only; callers who want a different list pass one explicitly via the alpn_protocols keyword argument.

source

Reseau.jl binds SSL_CTX_set_alpn_select_cb internally (at src/5_tls.jl:725-732 in Reseau v1.0.1), which is the exact upstream gap that blocks server-side h2 on OpenSSL.jl. This makes Reseau the recommended backend for server-side h2 TLS until OpenSSL.jl adds its own binding.

Symmetry-break: the Reseau helpers are constructors, not mutators. Reseau.TLS.Config is an immutable Julia struct (alpn_protocols is defensively copy()-ed at construction in Reseau v1.0.1 src/5_tls.jl:240), so an analogous set_alpn_h2!(::Reseau.TLS.Config) is structurally impossible. The reseau_h2_* helpers build fresh configs with alpn_protocols = PureHTTP2.ALPN_H2_PROTOCOLS pre-populated; callers override via an explicit alpn_protocols=... kwarg.

Extension-absent behavior

When neither OpenSSL.jl nor Reseau.jl is in the environment, all four helpers exist as generic functions with zero methods. Calling them throws MethodError — by design. PureHTTP2.jl's runtime dependency graph stays empty and the extensions are opt-in.

Current limitations

h2c is fully supported. If your deployment is inside a trusted network, behind a TLS-terminating proxy, or a gRPC service running over loopback, the serve_connection! + Sockets.TCPSocket pattern above is the intended shipping path.

Server-side h2 over TLS is supported via Reseau.jl — see the "Reseau.jl" subsection above. An analogous server-side helper in PureHTTP2OpenSSLExt awaits OpenSSL.jl's SSL_CTX_set_alpn_select_cb binding landing upstream. The upstream tracking entry in upstream-bugs.md is marked worked-around via Reseau.jl.

Client-role PureHTTP2.jl code shipped at Milestone 6. Both backends' helpers work with open_connection! on the client side.