HTTP/3#

The HTTP/3 client (RFC 9114) runs over a QUIC stack (RFC 9000/9001/9002) and QPACK codec (RFC 9204) implemented in this repository — no quic-go, no cgo. Only the TLS 1.3 handshake itself comes from the standard library crypto/tls; packet protection, loss recovery, congestion control, and flow control are poseidon code.

The request API is the same Do / DoStream you use with HTTP/2. A client.Request and client.Response do not change between transports; switching a load test from H2 to H3 is a constructor change.

Example#

examples/http3/main.go:

// Command http3-example issues a single HTTP/3 GET over QUIC with the poseidon
// client, and shows how to opt into BBR congestion control.
//
//	go run ./examples/http3
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"time"

	"github.com/lodgvideon/poseidon-http-client/client"
	"github.com/lodgvideon/poseidon-http-client/quic"
)

func main() {
	// The simple path: one QUIC connection, buffered response.
	//
	//	c, err := client.NewH3Client("www.cloudflare.com:443",
	//	    &tls.Config{ServerName: "www.cloudflare.com"})
	//
	// Below uses NewClient so we can also select BBR congestion control.
	c, err := client.NewClient(client.ClientOptions{
		Addr:      "www.cloudflare.com:443",
		Transport: client.TransportH3,
		TLSConfig: &tls.Config{ServerName: "www.cloudflare.com"},
		H3ConnOptions: []quic.ConnOption{
			quic.WithCongestionControl(quic.CCBBR), // default is NewReno
		},
	})
	if err != nil {
		log.Fatalf("build client: %v", err)
	}
	defer func() { _ = c.Close() }()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	resp := &client.Response{}
	if err := c.Do(ctx, client.GET("/"), resp); err != nil {
		log.Fatalf("GET /: %v", err)
	}
	fmt.Printf("HTTP/3 %d — %d bytes\n", resp.Status, len(resp.Body))
}

Constructors#

Three constructors, mirroring the HTTP/2 set:

// One QUIC connection. Buffered (Do) and streaming (DoStream) requests.
client.NewH3Client(addr string, tlsConfig *tls.Config, opts ...Option) (*Client, error)

// Pool of QUIC connections to one host.
client.NewH3PoolClient(addr string, tlsConfig *tls.Config, pool PoolOptions, opts ...Option) (*Client, error)

// Service discovery: a Resolver supplies addresses, requests spread across them.
client.NewManagedH3Client(resolver Resolver, tlsConfig *tls.Config, opts ...Option) (*Client, error)

For settings the constructors do not expose — such as congestion control — build with client.NewClient(client.ClientOptions{...}) and set Transport: client.TransportH3, as in the example above.

Concurrent requests#

One HTTP/3 connection carries multiple in-flight requests at once, each on its own QUIC stream. Issue Do / DoStream from concurrent goroutines against the same client; the pool and managed variants additionally spread streams across connections.

Cipher suites#

All three TLS 1.3 AEAD suites are supported for QUIC packet protection: AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305. The server picks during the handshake; no configuration is needed on the client. A suite outside this set (for example TLS_AES_128_CCM_8_SHA256) fails at key install with the typed error quic.ErrCryptoSuite — no hang, no panic.

QPACK#

Header compression uses dynamic-table QPACK in both directions: the client encodes request headers against the dynamic table and decodes server insertions on the decoder stream. This happens automatically; there is no knob to turn.

Allocations#

The HTTP/3 wire codec is zero-alloc: QUIC frames and packet headers, HTTP/3 frames, and QPACK field sections encode and decode at 0 B/op, 0 allocs/op. The same CI bench gate that enforces this for the HTTP/2 frame and HPACK codec covers the qpack, quic, and http3 packages. The QUIC packet send path is the exception: building and sealing an outgoing packet allocates a small, bounded amount per packet. A request over HTTP/3 is therefore low-alloc, not allocation-free.

Congestion control#

The default is NewReno. BBR is available opt-in:

H3ConnOptions: []quic.ConnOption{
	quic.WithCongestionControl(quic.CCBBR),
},

BBR ships correct and tested, but its throughput advantage over NewReno only shows on a bottlenecked WAN path with real queuing delay. On a LAN or loopback you will not measure a difference. If you cannot benchmark your target path, keep the NewReno default.

Linux batched I/O#

On Linux the QUIC transport uses GSO (generic segmentation offload) to hand the kernel batches of outgoing packets in one syscall, and GRO to receive coalesced batches. Both are automatic — no configuration, no build tags. On other platforms the client sends and receives one datagram per syscall.

Non-goals#

Deliberately out of scope for 1.0:

  • 0-RTT / session resumption — every connection does a full handshake.
  • QUIC connection migration — a connection is bound to the path it was opened on.
  • HTTP/3 server push — never enabled.

The client never initiates any of these. A peer offering them is simply not engaged; nothing fails, no error surfaces. The one hard failure mode in this area is an unsupported cipher suite, which returns the typed quic.ErrCryptoSuite described above.