# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.0.1] - 2026-04-15
### Fixed
- h3: consult `stream_type_handler` on fresh peer-initiated bidi
streams so extensions can claim them before default request
handling (#62)
- docs: `rebar3 ex_doc` now runs clean (#63)
## [1.0.0] - 2026-04-15
First release with HTTP/3. Brings full client + server HTTP/3
(RFC 9114) with QPACK (RFC 9204), HTTP Datagrams (RFC 9297),
Server Push, Extensible Priorities, Extended CONNECT, and the
extension-stream hooks WebTransport needs. Also a critical
flow-control deadlock fix in the QUIC core, a BBR loopback
throughput fix, and the H3 server owner default change.
### HTTP/3 (`quic_h3`, new module)
#### Added
- HTTP/3 client and server (RFC 9114) with QPACK header compression
(RFC 9204): request/response, body data, trailers, GOAWAY,
cancellation, CLI tools (`bin/quic_h3c`, `bin/quic_h3d`)
- Server Push (RFC 9114 §4.6): `push/3`, `send_push_response/4`,
`send_push_data/4`, `set_max_push_id/2`, `cancel_push/2`
- Extensible Priorities (RFC 9218): `priority` request option,
PRIORITY_UPDATE frames, urgency / incremental hints
- Extended CONNECT (RFC 9220) for WebTransport-style upgrades
- HTTP Datagrams (RFC 9297): `send_datagram/3`,
`h3_datagrams_enabled/1`, `max_datagram_size/2`, capsule framing
- Extension-stream hook: `stream_type_handler` option on
`start_server/3` claims peer-initiated uni and bidi streams whose
first varint matches a caller-supplied filter; claimed bytes are
delivered as `{stream_type_data, ...}` owner messages instead of
being parsed as HTTP/3 requests. Owner also receives
`stream_type_open`, `stream_type_closed`, `stream_type_reset`,
`stream_type_stop_sending` events
- Client-initiated extension streams: `quic_h3:open_bidi_stream/1,2`
pre-claims a bidi stream with a signal-type varint (e.g.
WebTransport's `0x41`) so inbound bytes route through the
claimed-bidi path
- Per-connection owner override via `connection_handler` callback on
`start_server/3` for hosting many sessions per listener
- Per-stream handler registration: `set_stream_handler/3,4`,
`unset_stream_handler/2` to redirect body data to a worker pid
- Query API: `get_settings/1`, `get_peer_settings/1`,
`get_quic_conn/1`
- Documentation: `docs/HTTP3.md` reference + benchmarks section
- E2E test infrastructure: `quic_h3_e2e_SUITE`, `quic_h3_h3spec_SUITE`,
`quic_h3_owner_SUITE`; dedicated CI job
- Performance benchmark: `quic_h3_bench`
#### Changed
- Server connection owner now defaults to the listener gen_server
(long-lived, trap_exit'ed) instead of the `start_server` caller
pid; durable owners for datagram / stream-type events should be
supplied via the per-connection `connection_handler` callback
- SETTINGS directionality validation tightened to RFC 9114
#### Fixed
- Server connections wedged with `connect_timeout` when the process
that called `start_server/3` exited before a client arrived and
either `h3_datagram_enabled` or `stream_type_handler` was set
- Discard unknown unidirectional stream payload (RFC 9114 §6.2
unknown-stream-type rule) instead of erroring the connection
- Emit trailing empty DATA event when response carries FIN so owners
always see `Fin = true` exactly once
- Strict PRIORITY_UPDATE frame parsing per RFC 9218
- DoS hardening on header / capsule / frame parsing
- Header / trailer / `:path` / `:status` symmetry between client and
server validation
- GOAWAY drain enforcement: reject new requests after a GOAWAY is
sent or received
- Server push lifecycle correctness (PUSH_PROMISE pairing, duplicate
detection, MAX_PUSH_ID enforcement)
- Tighten RFC 9114 / 9204 compliance across multiple parsers
- `sync` option on `connect/3` resolves an E2E race where the client
tried to send before SETTINGS exchange completed
- Improved frame error handling and header validation
- aioquic SETTINGS compatibility
- QPACK: encoder eviction guard prevents references to
unacknowledged dynamic-table entries; rejects `Increment = 0`
### QUIC transport
#### Added
- Spin bit (RFC 9000 §17.4)
- Stateless reset support (RFC 9000 §10.3)
- Full NEW_TOKEN issuance and validation loop
- `RESET_STREAM_AT` transport parameter and frame plumbing
- `quic:set_congestion_control/2` runtime CC switch API
- `quic:get_peer_transport_params/1` introspection API
#### Changed
- BBR internal clock switched to microseconds; loopback transfers no
longer pin to the InitialRtt fallback
#### Fixed
- Stream-level `MAX_STREAM_DATA` window stopped sliding once
`recv_max_data` reached `fc_max_receive_window` (8 MB default).
Past the cap, the auto-tune re-sent the same value forever and the
sender stalled at 8 MB lifetime per stream. The window now slides
past `recv_offset` like the connection-level window already does
- BBR loopback throughput regression: ms-precision clock collapsed
delivery-rate intervals to 0/1 ms and clamped BDP to the 4-packet
minimum, holding throughput at ~0.03 Mbps. Microsecond-precision
internal clock restores expected behavior
- Send `MAX_STREAMS` as peer-initiated streams complete
(RFC 9000 §4.6); previously peers could exhaust the stream-id space
### Distribution (`quic_dist`)
#### Added
- User-accessible streams API: `quic_dist:open_stream/1,2`, `send/3`,
`close_stream/1`, `reset_stream/1,2`, `controlling_process/2`,
`list_streams/0,1`, with acceptor pool and stream priorities
- Connection migration logging
- Distributed Erlang benchmarks + multi-node test scripts
- Per-iteration latency stats in throughput benchmark (min/p50/p99/max
+ timeout counts)
#### Changed
- Test runner logs each test's results as it returns rather than at
the end, so a stalled middle test no longer hides the others
### Tests and infrastructure
- `quic_e2e_*_SUITE` and `quic_h3_e2e_SUITE` run against in-process
servers; Docker no longer required for these jobs
## [0.11.0] - 2026-04-09
### Added
- Full QUIC connection migration support (RFC 9000 Section 9)
- Server-side address change detection (NAT rebinding vs active migration)
- Path validation with PATH_CHALLENGE/PATH_RESPONSE
- CID rotation for path unlinkability
- `disable_active_migration` transport parameter
- Application error code support for CONNECTION_CLOSE frames
- Client certificate support (`verify` server option)
- CUBIC congestion control (RFC 9438)
- BBR congestion control
- HyStart++ slow start (RFC 9406) for all CC algorithms
- UDP packet batching with GSO/GRO support
- Configurable UDP buffer sizing (recbuf/sndbuf options)
- QLOG tracing for debug visibility
- Pluggable congestion control behavior
- Stream deadlines for per-stream timeout control
- STOP_SENDING API (`quic:stop_sending/3`)
- `max_udp_payload_size` transport parameter
- Async send API and socket receive optimizations
- Throughput benchmarks (`quic_throughput_bench`, `quic_batch_bench`)
- QUIC-based Erlang distribution (`quic_dist`) for node communication over QUIC
- Distribution modules: `quic_dist`, `quic_dist_controller`, `quic_dist_sup`
- EPMD replacement module (`quic_epmd`) for QUIC-based node discovery
- Discovery backends: `quic_discovery_static` (static config), `quic_discovery_dns` (DNS SRV)
- Session ticket storage (`quic_dist_tickets`) for 0-RTT reconnection
- Stream prioritization for distribution: control stream (urgency 0), data streams (urgency 4-6)
- Backpressure mechanism for distribution congestion control
- Keep-alive PING frames for transport-level liveness (configurable via `keep_alive_interval`)
- `quic:get_stats/1` API for connection packet counts (used for liveness detection)
- `quic:send_ping/1` API for transport-level PING frames
- RTT-based flow control auto-tuning for improved throughput
- Packet pacing (RFC 9002 Section 7.7) to prevent bursts
### Changed
- ConnRef is now connection PID (simpler API)
- Improved ACK processing performance (O(n^2) to O(n) with gb_sets)
- Timer batching for reduced overhead
- Zero-copy packet processing optimizations
- Distribution liveness detection now uses QUIC packet counts instead of application ticks
- Improved congestion control with quic-go-inspired settings (larger initial cwnd)
- Flow control windows auto-tune based on RTT measurements
### Fixed
- Throughput regression in connection migration (wasteful binary allocation)
- CUBIC cwnd collapse issue
- BBR delivery rate interval causing cwnd collapse
- BBR initial pacing rate causing transfer hangs
- Pacing precision loss causing transfer stalls
- Various RFC compliance fixes for QUIC connection migration
- `net_tick_timeout` errors under heavy load by using QUIC-level activity as liveness proof
- Stream flow control `recv_max_data` using wrong limits
- Distribution controller backpressure data loss
- Congestion control protocol compliance issues
- Recovery exit when only non-ack-eliciting packets are ACKed
- Tick timeout issues in distribution controller
- Flow control blocking that caused deadlocks
- Message framing for large message transfers
### Removed
- NAT traversal support from `quic_dist` (use standard QUIC connection migration instead)
## [0.10.2] - 2026-02-21
### Fixed
- Deprecated `catch` expressions replaced with `try...catch...end`
- Undefined `dynamic()` type replaced with `term()` in type specs
- CI workflow consolidated with separate unit-tests, e2e, and interop jobs
## [0.10.1] - 2026-02-21
### Fixed
- ACK range encoding crash for out-of-order packets: when packets arrived out
of order (e.g., 10, 5, 6), ACK ranges were not properly maintained in
descending order or merged, causing negative Gap values that crashed
`quic_varint:encode/1` with `badarg`
## [0.10.0] - 2026-02-21
### Added
- RFC 9312 QUIC-LB Connection ID encoding support for load balancer routing
- New `quic_lb` module with three encoding algorithms:
- Plaintext: server_id visible in CID (no encryption)
- Stream Cipher: AES-128-CTR encryption of server_id
- Block Cipher: 4-round Feistel network for <16 bytes, AES-CTR for 16 bytes,
truncated cipher for >16 bytes
- `#lb_config{}` record for LB configuration (algorithm, server_id, key, nonce_len)
- `#cid_config{}` record for CID generation configuration
- `lb_config` option in `quic_listener` to enable LB-aware CID generation
- Variable DCID length support in short header packet parsing
- LB-aware CID generation in `quic_connection` for NEW_CONNECTION_ID frames
- E2E test suite `quic_lb_e2e_SUITE` with 21 integration tests
- `quic:server_spec/3` to get a child spec for embedding QUIC servers in custom
supervision trees
- Stream reassembly test suite `quic_stream_reassembly_SUITE` for ordered delivery
verification
### Changed
- `quic:set_owner/2` is now asynchronous (cast instead of call)
### Fixed
- `quic:get_server_port/1` now returns the actual OS-assigned port when server
was started with port 0 (ephemeral port), instead of returning 0
- `quic:get_server_connections/1` now correctly returns connection PIDs; was
returning empty list due to `get_listeners/1` returning supervisor pids
instead of actual listener processes
- Removed redundant `link/1` call in listener (connection already linked via
`gen_statem:start_link`)
- Unhandled calls in connection state machine now return `{error, {invalid_state, State}}`
instead of silently timing out
- Server-side connection termination no longer closes shared listener socket:
previously when a server connection terminated, it would close the UDP socket
shared with the listener, breaking all subsequent connections
- Cancel delayed ACK timer in connection terminate to prevent timer messages
to dead processes
- Session ticket table now has TTL (7 days) and size limit (10,000 entries) to
prevent unbounded memory growth
- Listener now properly cleans up ETS tables on terminate (standalone mode only,
pool mode tables are managed by the pool manager)
- Draining state now uses calculated `3 * PTO` timeout per RFC 9000 Section 10.2
instead of hardcoded 3 seconds
- Pre-connection pending data queue now has size limit (1000 entries) to prevent
memory exhaustion from slow handshakes
- Buffer contiguity calculation now has iteration limit to prevent stack overflow
with highly fragmented receive buffers
- Stream data is now properly reassembled before delivery: previously data was
delivered immediately as received, causing corruption when packets arrived out
of order during large file transfers. Data is still streamed incrementally as
contiguous chunks become available
- Server connections no longer modify listener's socket active state: server-side
connections were calling `inet:setopts(Socket, [{active, once}])` on the shared
listener socket, overriding the listener's `{active, N}` configuration and
causing the socket to go passive after receiving packets
## [0.9.0] - 2026-02-20
### Added
- Multi-pool server support with ranch-style named server pools
- `quic:start_server/3` to start named server with connection pooling
- `quic:stop_server/1` to stop named server
- `quic:get_server_info/1` to get server information (pid, port, opts, started_at)
- `quic:get_server_port/1` to get server listening port
- `quic:get_server_connections/1` to get server connection PIDs
- `quic:which_servers/0` to list all running servers
- Application supervision structure (`quic_app`, `quic_sup`, `quic_server_sup`)
- ETS-based server registry (`quic_server_registry`) with process monitoring
- `pool_size` option for listener process pooling with SO_REUSEPORT
- FreeBSD CI testing workflow
- Expanded Linux CI matrix (Ubuntu 22.04/24.04, OTP 26-28)
### Changed
- `quic.app.src` now includes `{mod, {quic_app, []}}` for OTP application behaviour
- Listener supervisor registers with server registry on init for restart recovery
## [0.8.0] - 2026-02-20
### Added
- Stream prioritization (RFC 9218): urgency-based scheduling with 8 priority
levels (0-7) and incremental delivery flag
- `quic:set_stream_priority/4` and `quic:get_stream_priority/2` API
- Bucket-based priority queue for O(1) stream scheduling
- Preferred address handling (RFC 9000 Section 9.6): server can advertise a
preferred address during handshake, client validates via PATH_CHALLENGE and
automatically migrates to validated preferred address
- `preferred_ipv4` and `preferred_ipv6` listener options for server configuration
- `#preferred_address{}` record for IPv4/IPv6 addresses, CID, and reset token
- `quic_tls:encode_preferred_address/1` and `quic_tls:decode_preferred_address/1`
- Idle timeout enforcement (RFC 9000 Section 10.1): when `idle_timeout` option
is set, internal timer automatically closes connection after timeout with no
activity (set to 0 to disable)
- Persistent congestion detection (RFC 9002 Section 7.6): detects prolonged packet
loss spanning > PTO * 3 and resets cwnd to minimum window
- Frame coalescing: ACK frames are coalesced with small pending stream data
(< 500 bytes) for more efficient packet utilization
## [0.7.1] - 2026-02-20
### Fixed
- Packet number reconstruction per RFC 9000 Appendix A: truncated packet numbers
are now properly reconstructed using the largest received PN, fixing decryption
failures for large responses (>255 packets with 1-byte PN encoding)
## [0.7.0] - 2026-02-20
### Added
- Docker interop runner integration (client and server images)
- Session resumption interop test (`resumption`)
- 0-RTT early data interop test (`zerortt`)
- Connection migration interop test (`connectionmigration`)
- `quic:migrate/1` API for triggering active path migration
- All 10 QUIC Interop Runner test cases now pass:
- handshake, transfer, retry, keyupdate, chacha20, multiconnect, v2,
resumption, zerortt, connectionmigration
### Fixed
- Connection-level flow control: now properly tracks `data_received` and sends
MAX_DATA frames when 50% of connection window is consumed (RFC 9000 Section 4.1)
- Large downloads: interop client now writes to disk incrementally (streaming)
instead of accumulating in memory
- Server DCID initialization: server now correctly sets DCID from client's
Initial packet SCID field, fixing short header packet alignment
- Key update HP key preservation: header protection keys are no longer rotated
during key updates per RFC 9001 Section 6.6
- Fixed bit validation: skip padding bytes (0x00) and invalid short headers
(fixed bit not set) in coalesced packets
- Role-based key selection in 1-RTT packet decryption
## [0.6.5] - 2026-02-19
### Added
- `quic_listener:start/2` for unlinked listener processes
- `set_owner` call handling in idle and handshaking states
### Fixed
- IPv4/IPv6 address family matching when opening client sockets
- Race condition: transfer socket ownership before sending packet
- Handle header unprotection errors gracefully in packet decryption
- Removed verbose debug logging from listener
## [0.6.4] - 2026-02-17
### Fixed
- Server now selects correct signature algorithm based on key type (EC vs RSA)
## [0.6.3] - 2026-02-17
### Fixed
- Fixed transport params parsing in ClientHello - properly unwrap {ok, Map} result
## [0.6.2] - 2026-02-17
### Fixed
- Fixed key selection for all packet types based on role (server vs client)
- Server now uses correct keys for both sending and receiving packets
- Fixed Initial, Handshake, and 1-RTT packet encryption/decryption
## [0.6.1] - 2026-02-17
### Fixed
- Server-side packet decryption now uses correct keys (client keys for Initial/Handshake packets received from clients)
## [0.6.0] - 2026-02-17
### Added
- DATAGRAM frame support (RFC 9221) for unreliable data transmission
- `quic:set_owner/2` to transfer connection ownership (like gen_tcp:controlling_process/2)
- `quic:peercert/1` to retrieve peer certificate (DER-encoded)
- `quic:send_datagram/2` to send QUIC datagrams
- Connection handler callback in `quic_listener` for custom connection handling
- ACK delay for datagram-only packets per RFC 9221 Section 5.2
- Proper ACK generation at packet level for all ack-eliciting frames
### Fixed
- Datagrams are not retransmitted on loss (RFC 9221 compliance)
- ACKs now sent for all ack-eliciting frames, not just stream data
## [0.5.1] - 2026-02-17
### Fixed
- Pad payload for header protection sampling to prevent crashes during PTO timeout
## [0.5.0] - 2026-02-17
### Added
- Retry packet handling (RFC 9000 Section 8.1)
- Stateless reset support (RFC 9000 Section 10.3)
- Connection ID limit enforcement (RFC 9000 Section 5.1.1)
- ECN support for congestion control (RFC 9002 Section 7.1)
- RFC 9000/9001 test vectors
- Interoperability test suite with quic-go server
- E2E tests in CI pipeline
### Fixed
- CI compatibility with OTP 28 (use rebar3 nightly)
- quic-go Docker build (pin to v0.48.2)
## [0.4.0] - 2025-02-17
### Changed
- Moved `doc/` to `docs/` to prevent ex_doc from overwriting documentation
- Consolidated `hash_len/1` and `cipher_to_hash/1` functions in `quic_crypto` module
- Refactored key derivation in `quic_keys` using `cipher_params/1` helper
- Improved socket cleanup on initialization failure in `quic_connection`
### Removed
- Removed `send_headers/4` API (HTTP/3 functionality, not core QUIC transport)
### Fixed
- Added bounds checking for header protection sample extraction in `quic_aead`
- Added CID length validation (max 20 bytes per RFC 9000) in `quic_packet`
- Added token length validation in `quic_packet`
- Added frame data length limits in `quic_frame` to prevent memory exhaustion
- Added ACK range limits in `quic_ack` to prevent DoS attacks
- Fixed weak random: use `crypto:strong_rand_bytes/1` for ticket age_add
- Fixed dialyzer warning in `quic_tls` by adding error handling to `decode_transport_params/1`
## [0.3.0] - 2025-02-16
### Added
- Server mode with `quic_listener` module
- 0-RTT early data support (RFC 9001 Section 4.6)
- Connection migration support (RFC 9000 Section 9)
- Key update support (RFC 9001 Section 6)
## [0.2.0] - 2025-02-15
### Added
- Stream multiplexing (bidirectional and unidirectional)
- Flow control (connection and stream level)
- Congestion control (NewReno)
- Loss detection and packet retransmission (RFC 9002)
## [0.1.0] - 2025-02-14
### Added
- Initial release
- TLS 1.3 handshake (RFC 8446)
- Basic QUIC transport (RFC 9000)
- AEAD packet protection (RFC 9001)