Skip to content

v0.0.25 - One socket, many QUIC connections 🌐

Choose a tag to compare

@Kludex Kludex released this 24 Aug 11:08
100fe07

🌐 One socket, many QUIC connections

zttp 0.0.25 gives HTTP/3 servers a shared-socket control plane. QuicEndpoint routes QUIC connections, answers unsupported versions, performs stateless Retry, tracks connection IDs, and aggregates timers without taking ownership of your UDP socket or clock.

pip install --upgrade zttp==0.0.25

✨ Highlights

  • A sans-I/O QUIC endpoint. QuicEndpoint routes long and short headers to the correct H3Connection, retains state only after authenticating an Initial, bounds active connections, and produces addressed OutboundDatagram values. Optional Retry tokens bind the client address, original destination connection ID, Retry source connection ID, and expiry without retaining per-client state (#204).
  • Backpressure follows the application. HTTP/3 DATA receive credit is now returned through H3Connection.consume_data(), after the application accepts bytes, rather than when the parser advances. Frame overhead remains automatic, and reset paths safely release unused credit (#206).
  • Explicit QUIC configuration and routing. QuicTransportParameters provides named, range-checked transport settings. local_connection_ids() exposes active local IDs, and endpoint-owned issuance keeps shared-socket routes synchronized (#203, #205).
  • Complete certificate chains. HTTP/3 credentials can send an ordered leaf-and-intermediate chain while preserving the single-certificate form (#202).
  • Buffers go straight in. Every receive path now accepts contiguous buffer-protocol objects, including bytes, bytearray, and memoryview. Immutable bytes keep the HTTP/1 zero-copy fast path, while mutable input is copied before its buffer export is released (#212).

🔌 Serving a shared UDP socket

import time

import zttp

endpoint = zttp.QuicEndpoint(
    retry=True,
    token_secret=b"replace-with-at-least-32-secret-bytes",
)


def now_us() -> int:
    return time.monotonic_ns() // 1_000


def receive(datagram: bytes, peer_address: bytes) -> zttp.H3Connection | None:
    connection = endpoint.receive_datagram(datagram, peer_address, now_us())
    for outbound in endpoint.data_to_send():
        send_datagram(outbound.data, outbound.peer_address)
    return connection

The endpoint remains sans-I/O. Your application owns the socket, address encoding, monotonic clock, and timer scheduling. Use next_timeout() and handle_timeout() to drive loss recovery and idle timeouts, then drain data_to_send() after each receive, timeout, or endpoint control call.


⚠️ HTTP/3 migration notes

Return flow-control credit after your application consumes each Data event. A sender may stall when you omit this call:

if isinstance(event, zttp.Data):
    deliver(event.data)
    connection.consume_data(event.stream_id, len(event.data))

Raw encoded transport_params are replaced by a typed dictionary with validated values:

transport_params: zttp.QuicTransportParameters = {
    "initial_max_data": 1_048_576,
    "initial_max_streams_bidi": 100,
}
connection = zttp.Connection(
    zttp.SERVER,
    zttp.HTTP3,
    transport_params=transport_params,
)

For certificate chains, place the leaf first and intermediates after it:

credentials: zttp.TlsCredentials = {
    "certificates": (leaf_certificate, intermediate_certificate),
    "private_key": leaf_private_key,
}

data_to_send_with_addresses() now returns OutboundDatagram objects. Read .data and .peer_address instead of unpacking two-item tuples.

HTTP/1.1 and HTTP/2 integrations require no changes. HTTP/3 remains experimental.


📦 Wheels

Binary wheels cover CPython 3.10 through 3.15 on Linux, macOS, and Windows, including free-threaded 3.14 and 3.15 builds. Free-threaded interpreters still enable the compatibility GIL when importing zttp.


Full changelog: v0.0.24...v0.0.25