AIOQUIC(1)

๐Ÿ“ฆ
version 1.3.0 ยท 2 versions
, in fedora 44

๐ŸŽ€ AIOQUIC(1) ๐ŸŽ€

Okay bestie, this is the page for aioquic ๐Ÿ’–, a library for the QUIC network protocol in Python and it is โœจ so good โœจ.

โœจ NAME โœจ

aioquic - aioquic ๐Ÿฆ‹ The badges are lowkey iconic, and each one has an unlabelled image beside it:

So, aioquic is a library for the QUIC network protocol in Python ๐ŸŒธ, and it comes with several APIs:

  • a QUIC API following the "bring your own I/O" pattern, suitable for embedding in any framework, ๐Ÿ’…
  • an HTTP/3 API which also follows the "bring your own I/O" pattern, ๐Ÿงš
  • a QUIC convenience API built on top of asyncio (https://docs.python.org/3/library/asyncio.html#module-asyncio), Python's standard asynchronous I/O framework. ๐Ÿ‘‘

๐Ÿ’– DESIGN ๐Ÿ’–

Sans-IO APIs

Both the QUIC and the HTTP/3 APIs follow the sans I/O pattern (https://sans-io.readthedocs.io/) ๐Ÿซถ, which means they leave the actual I/O operations to the API user. This approach has a number of advantages, including making the code testable and allowing integration with different concurrency models. It's giving clean architecture, no cap โœจ.

TLS and encryption

TLS 1.3

aioquic features a minimal TLS 1.3 implementation built upon the cryptography library (https://cryptography.io/) ๐Ÿ”. Why not just use the normal stuff? Because QUIC needs some APIs that are currently unavailable in mainstream TLS implementations such as OpenSSL ๐Ÿ™ˆ:

  • the ability to extract traffic secrets
  • the ability to operate directly on TLS messages, without using the TLS record layer

Header protection and payload encryption

QUIC makes extensive use of cryptographic operations to protect QUIC packet headers and encrypt packet payloads ๐Ÿ”ฅ. These operations happen for every single packet, and they are a determining factor for performance. That's why they are implemented as a C extension linked to OpenSSL (https://www.openssl.org/). Main character energy for performance ๐Ÿ’œ.

๐Ÿงš QUIC API ๐Ÿงš

The QUIC API performs no I/O on its own, leaving that to the API user. This means you can integrate QUIC in any Python application, regardless of the concurrency model you're using ๐Ÿฆ‹.

Connection

The class signature is:

aioquic.quic.connection.QuicConnection(*, configuration, original_destination_connection_id=None, retry_source_connection_id=None, session_ticket_fetcher=None, session_ticket_handler=None, token_handler=None)

A QUIC connection. โœจ

The state machine is driven by three kinds of sources:

  • the API user requesting data to be sent out (see connect(), reset_stream(), send_ping(), send_datagram_frame() and send_stream_data()) ๐Ÿ’Œ
  • data being received from the network (see receive_datagram()) ๐Ÿ“ฉ
  • a timer firing (see handle_timer()) โฐ

Parameters

  • configuration (QuicConfiguration) โ€” the QUIC configuration to use. ๐ŸŽ€

change_connection_id()

Switch to the next available connection ID and retire the previous one. Bestie, this one is a glow-up ๐ŸŒˆ.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Return type: None

close(error_code=QuicErrorCode.NO_ERROR, frame_type=None, reason_phrase='')

Close the connection. Bye bye, connection ๐Ÿ‘‹.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Parameters

  • error_code (int) โ€” an error code telling why the connection is being closed. ๐Ÿšช
  • reason_phrase (str) โ€” a human-readable explanation of why the connection is being closed. ๐Ÿ“

Return type: None

connect(addr, now)

Initiate the TLS handshake. This can only be called for clients, and only a single time. Ngl, that's a strict vibe ๐Ÿ™…โ€โ™€๏ธ.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Parameters

  • addr (Any) โ€” the network address of the remote peer. ๐ŸŒ
  • now (float) โ€” the current time. โฑ๏ธ

Return type: None

datagrams_to_send(now)

Return a list of (data, addr) tuples of datagrams which need to be sent, plus the network address each one needs to be sent to ๐Ÿ“ฆ.

After calling this method, call get_timer() to find out when the next timer needs to be set ๐Ÿซถ.

Parameters

  • now (float) โ€” the current time. โฑ๏ธ

Return type: list[tuple[bytes, Any]]

get_next_available_stream_id(is_unidirectional=False)

Return the stream ID for the next stream created by this endpoint ๐Ÿ†”.

Return type: int

get_timer()

Return the time at which the timer should fire, or None if no timer is needed. โฐ Simple and iconic.

Return type: Optional[float]

handle_timer(now)

Handle the timer. Periodt โฐ.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Parameters

  • now (float) โ€” the current time. โฑ๏ธ

Return type: None

next_event()

Retrieve the next event from the event buffer. It returns None if there are no buffered events, so don't panic if it's empty ๐Ÿซ .

Return type: Optional[QuicEvent]

receive_datagram(data, addr, now)

Handle an incoming datagram ๐Ÿ“ฌ.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Parameters

  • data (bytes) โ€” the datagram which was received. ๐Ÿ“ฆ
  • addr (Any) โ€” the network address from which the datagram was received. ๐ŸŒ
  • now (float) โ€” the current time. โฑ๏ธ

Return type: None

request_key_update()

Request an update of the encryption keys. Key rotation hits different ๐Ÿ”‘.

Note: After calling this method you need to call the QUIC connection datagrams_to_send() method to retrieve data which needs to be sent over the network. If you are using the asyncio API, calling the transmit() method (aioquic.asyncio.QuicConnectionProtocol.transmit) will do it for you. ๐Ÿ’…

Return type: None

reset_stream(stream_id, error_code)

Abruptly terminate the sending part of a stream. Yikes, no warning, just vibes ๐Ÿ’€.

This method has no effect if a reset has already been triggered, either by a call to reset_stream() or by the reception of a STOP_SENDING frame. ๐Ÿ™…โ€โ™€๏ธ

Okay bestie, the good stuff is here ๐Ÿฆ‹ Ready for the rest of the methods?

๐Ÿšจ Heads up, bestie: after calling this one you still have to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network ๐Ÿ“ก If you're on the asyncio API, calling transmit() (that's aioquic.asyncio.QuicConnectionProtocol.transmit) does that step for you ๐Ÿ’–

Parameters

  • stream_id (int) โ€” the stream's ID ๐ŸŒธ
  • error_code (int) โ€” an error code explaining why the stream is being reset ๐Ÿ’€

Return type: None

send_datagram_frame(data) ๐Ÿฆ‹

Sends a DATAGRAM frame ๐Ÿ“จ

๐Ÿšจ Heads up, bestie: after calling this one you still have to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network ๐Ÿ“ก If you're on the asyncio API, calling transmit() (that's aioquic.asyncio.QuicConnectionProtocol.transmit) does that step for you ๐Ÿ’–

Parameters

  • data (bytes) โ€” the data you want to send ๐ŸŽ€

Return type: None

send_ping(uid) ๐Ÿ“

Sends a PING frame over to the peer ๐Ÿ‘€

๐Ÿšจ Heads up, bestie: after calling this one you still have to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network ๐Ÿ“ก If you're on the asyncio API, calling transmit() (that's aioquic.asyncio.QuicConnectionProtocol.transmit) does that step for you ๐Ÿ’–

Parameters

  • uid (int) โ€” a unique ID for this PING ๐Ÿ†”

Return type: None

send_stream_data(stream_id, data, end_stream=False) ๐Ÿงš

Sends data on a specific stream ๐Ÿ’œ

๐Ÿšจ Heads up, bestie: after calling this one you still have to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network ๐Ÿ“ก If you're on the asyncio API, calling transmit() (that's aioquic.asyncio.QuicConnectionProtocol.transmit) does that step for you ๐Ÿ’–

Parameters

  • stream_id (int) โ€” the stream's ID ๐ŸŒธ
  • data (bytes) โ€” the data you want to send ๐ŸŽ€
  • end_stream (bool) โ€” if you set this to True, the FIN bit gets set ๐Ÿ (the default is False)

Return type: None

stop_stream(stream_id, error_code) ๐Ÿ›‘

Requests termination of the receiving side of a stream ๐Ÿ™…โ€โ™€๏ธ

๐Ÿšจ Heads up, bestie: after calling this one you still have to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network ๐Ÿ“ก If you're on the asyncio API, calling transmit() (that's aioquic.asyncio.QuicConnectionProtocol.transmit) does that step for you ๐Ÿ’–

Parameters

  • stream_id (int) โ€” the stream's ID ๐ŸŒธ
  • error_code (int) โ€” an error code explaining why the stream is being stopped ๐Ÿ’…

Return type: None


โœจ Configuration โœจ

Here's where the settings live, so you can set up your connection exactly how you want it ๐ŸŽ€

aioquic.quic.configuration.QuicConfiguration

class aioquic.quic.configuration.QuicConfiguration(alpn_protocols=None, congestion_control_algorithm='reno', connection_id_length=8, idle_timeout=60.0, is_client=True, max_data=1048576, max_datagram_size=1200, max_stream_data=1048576, quic_logger=None, secrets_log_file=None, server_name=None, session_ticket=None, token=b'', cadata=None, cafile=None, capath=None, certificate=None, certificate_chain=<factory>, cipher_suites=None, initial_rtt=0.1, max_datagram_frame_size=None, original_version=None, private_key=None, quantum_readiness_test=False, supported_versions=<factory>, verify_mode=None)

This is your QUIC configuration class, the whole vibe for your connection ๐Ÿ‘‘

alpn_protocols: Optional[list[str]] = None ๐ŸŒˆ

A list of the ALPN protocols you support ๐Ÿ’–

congestion_control_algorithm: str = 'reno' ๐ŸŒŠ

The name of the congestion control algorithm you want to use ๐Ÿฆ‹

Currently supported algorithms are "reno" and "cubic" โœจ

connection_id_length: int = 8 ๐Ÿชช

The length in bytes of your local connection IDs ๐Ÿ”ข

idle_timeout: float = 60.0 โฐ

The idle timeout in seconds โณ

โš ๏ธ The connection gets terminated if nothing is received for the given duration, so don't ghost your peer for too long ๐Ÿ’€

is_client: bool = True ๐Ÿ™‹โ€โ™€๏ธ

Whether this is the client side of the QUIC connection ๐Ÿ’œ

load_cert_chain(certfile, keyfile=None, password=None) ๐Ÿ”

Loads a private key and its matching certificate ๐Ÿ—๏ธ

Return type: None

load_verify_locations(cafile=None, capath=None, cadata=None) ๐Ÿ“œ

Loads a set of "certification authority" (CA) certificates, which are used to validate other peers' certificates ๐Ÿ•ต๏ธโ€โ™€๏ธ

Return type: None

max_data: int = 1048576 ๐Ÿชฃ

The connection-wide flow control limit ๐Ÿšฆ

max_datagram_size: int = 1200 ๐Ÿ“ฆ

The maximum QUIC payload size in bytes to send, not counting UDP or IP overhead ๐Ÿ“

max_stream_data: int = 1048576 ๐ŸŽ€

The per-stream flow control limit ๐Ÿšฅ

quic_logger: Optional[QuicLogger] = None ๐Ÿ“

The QuicLogger instance you want to log events to ๐Ÿช„

secrets_log_file: Optional[TextIO] = None ๐Ÿ”‘

A file-like object where traffic secrets get logged ๐Ÿซฃ

This is super useful for analyzing traffic captures with Wireshark ๐Ÿฆˆ

server_name: Optional[str] = None ๐Ÿท๏ธ

The server name to use when verifying the server's TLS certificate. It can be either a DNS name or an IP address ๐ŸŒ

If it's a DNS name, it also gets sent during the TLS handshake in the Server Name Indication (SNI) extension ๐Ÿ’…

๐Ÿ“ Note: this is only used by clients ๐Ÿซถ

session_ticket: Optional[SessionTicket] = None ๐ŸŽŸ๏ธ

The TLS session ticket that should be used for session resumption ๐Ÿ”

token: bytes = b'' ๐Ÿช™

The address validation token that can be used to validate future connections ๐Ÿงฟ

๐Ÿ“ Note: this is only used by clients ๐Ÿซถ

aioquic.quic.logger.QuicLogger

class aioquic.quic.logger.QuicLogger

A QUIC event logger that stores traces in memory ๐Ÿง 

to_dict() ๐Ÿ“š

Returns the traces as a dictionary, which you can write out as JSON ๐Ÿ“ค

Return type: dict[str, Any]


โœจ Events โœจ

Events are how your connection tells you what just happened, so pay attention ๐Ÿ‘€

aioquic.quic.events.QuicEvent

class aioquic.quic.events.QuicEvent

The base class for all QUIC events ๐ŸŒŸ

aioquic.quic.events.ConnectionTerminated

class aioquic.quic.events.ConnectionTerminated(error_code, frame_type, reason_phrase)

This event fires when the QUIC connection gets terminated ๐Ÿ’”

error_code: int ๐Ÿšซ

The error code that was specified when the connection was closed ๐Ÿ”ข

frame_type: Optional[int] ๐Ÿงฉ

The frame type that caused the connection to close, or None ๐Ÿคทโ€โ™€๏ธ

reason_phrase: str ๐Ÿ’ฌ

The human-readable reason the connection was closed ๐Ÿ“–

aioquic.quic.events.HandshakeCompleted

class aioquic.quic.events.HandshakeCompleted(alpn_protocol, early_data_accepted, session_resumed)

This event fires when the TLS handshake completes, and that's a whole main-character moment ๐ŸŽ‰โœจ

Okay so the events are literally where the QUIC API gets its glow-up ๐Ÿ’… Here's the rest of the HandshakeCompleted attributes โœจ

  • alpn_protocol (Optional[str]) โ€” The protocol that got negotiated through ALPN, or None if nothing did ๐ŸŽ€
  • early_data_accepted (bool) โ€” Whether the remote peer said yes to early (0-RTT) data ๐Ÿš€
  • session_resumed (bool) โ€” Whether a TLS session got resumed ๐Ÿ”

class aioquic.quic.events.PingAcknowledged(uid)

The PingAcknowledged event pops off when a PING frame gets acknowledged. ๐Ÿ“

  • uid (int) โ€” The unique ID of that PING ๐Ÿ†”

class aioquic.quic.events.StopSendingReceived(error_code, stream_id)

The StopSendingReceived event fires when the remote peer basically says "hey, please stop sending data on this stream." ๐Ÿ™…โ€โ™€๏ธ

  • error_code (int) โ€” The error code the peer sent along with that request ๐Ÿ’€
  • stream_id (int) โ€” The ID of the stream the peer wants you to stop transmitting data on ๐Ÿ›‘

class aioquic.quic.events.StreamDataReceived(data, end_stream, stream_id)

The StreamDataReceived event fires every time data shows up on a stream. ๐Ÿ“ฌ

  • data (bytes) โ€” The data that got received ๐Ÿ“ฆ
  • end_stream (bool) โ€” Whether the STREAM frame had the FIN bit set. Basically, is this the goodbye message? ๐Ÿ‘‹
  • stream_id (int) โ€” The ID of the stream that the data came in on ๐Ÿงต

class aioquic.quic.events.StreamReset(error_code, stream_id)

The StreamReset event fires when the remote peer resets a stream. Rip ๐Ÿ˜ญ

  • error_code (int) โ€” The error code that caused the reset ๐Ÿซ 
  • stream_id (int) โ€” The ID of the stream that got reset ๐Ÿ”„

โœจ HTTP/3 API โœจ

Okay bestie, the HTTP/3 API does no I/O on its own ๐Ÿ™…โ€โ™€๏ธ It leaves all the sending and receiving to you, the API user. That means you can plug HTTP/3 into any Python app, no matter what concurrency model you're vibing with ๐ŸŒˆ

๐Ÿฆ‹ Connection

class aioquic.h3.connection.H3Connection(quic, enable_webtransport=False)

A low-level HTTP/3 connection object. It's giving raw power ๐Ÿ’ช

Parameters

  • quic (QuicConnection) โ€” A QuicConnection instance ๐Ÿง 

create_webtransport_stream(session_id, is_unidirectional=False)

Creates a WebTransport stream and hands you back its stream ID. ๐ŸŒŠ

Note: After calling this, you need to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network. If you're using the asyncio API, calling the transmit() method does that part for you. ๐Ÿซถ

Parameters

  • session_id (int) โ€” The WebTransport session identifier ๐Ÿ†”
  • is_unidirectional (bool) โ€” Whether you want a unidirectional stream (one-way, no cap) โžก๏ธ

Return type: int

handle_event(event)

Handles a QUIC event and returns a list of HTTP events. Main character energy โญ

Parameters

  • event (QuicEvent) โ€” The QUIC event you want to handle ๐ŸŽฏ

Return type: list[H3Event]

property received_settings: dict[int, int] | None

Returns the received SETTINGS frame, or None if there isn't one yet. โš™๏ธ

send_data(stream_id, data, end_stream)

Sends data on the given stream. ๐Ÿ“ค

Note: After calling this, you need to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network. If you're using the asyncio API, calling the transmit() method does that part for you. ๐Ÿซถ

Parameters

  • stream_id (int) โ€” The stream ID you're sending on ๐Ÿงต
  • data (bytes) โ€” The data you're sending ๐Ÿ“ฆ
  • end_stream (bool) โ€” Whether to end the stream here ๐Ÿ

Return type: None

send_datagram(stream_id, data)

Sends a datagram for the specified stream. Quick heads up: if the stream ID isn't a client-initiated bidirectional stream, an InvalidStreamTypeError exception gets raised. Not it ๐Ÿ™…โ€โ™€๏ธ

Note: After calling this, you need to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network. If you're using the asyncio API, calling the transmit() method does that part for you. ๐Ÿซถ

Parameters

  • stream_id (int) โ€” The stream ID ๐Ÿ†”
  • data (bytes) โ€” The HTTP/3 datagram payload ๐Ÿ“ฆ

Return type: None

send_headers(stream_id, headers, end_stream=False)

Sends headers on the given stream. ๐Ÿ’Œ

Note: After calling this, you need to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network. If you're using the asyncio API, calling the transmit() method does that part for you. ๐Ÿซถ

Parameters

  • stream_id (int) โ€” The stream ID you want the headers sent on ๐Ÿงต
  • headers (list[tuple[bytes, bytes]]) โ€” The HTTP headers to send ๐Ÿ“
  • end_stream (bool) โ€” Whether to end the stream ๐Ÿ

Return type: None

send_push_promise(stream_id, headers)

Sends a push promise related to the specified stream. ๐Ÿ”ฎ It returns the stream ID where headers and data can be sent next.

If the stream ID isn't a client-initiated bidirectional stream, an InvalidStreamTypeError exception is raised. Big oof ๐Ÿ˜ฌ

If there aren't any available push IDs, a NoAvailablePushIDError exception is raised. Also a big oof ๐Ÿ’€

Note: After calling this, you need to call the QUIC connection's datagrams_to_send() method to grab the data that needs to go out over the network. If you're using the asyncio API, calling the transmit() method does that part for you. ๐Ÿซถ

Parameters

  • stream_id (int) โ€” The stream ID the push promise is related to ๐Ÿงต
  • headers (list[tuple[bytes, bytes]]) โ€” The HTTP request headers for this push ๐Ÿ“จ

Return type: int

The last bit of the connection stuff ๐Ÿ’… property sent_settings: dict[int, int] | None hands you back the SETTINGS frame that was sent, or None if there isn't one ๐ŸŒธ

โœจ Events โœจ

class aioquic.h3.events.H3Event ๐Ÿ‘‘

This is the base class for all HTTP/3 events. Every other event below builds on it.

class aioquic.h3.events.DatagramReceived(data, stream_id) ๐Ÿฆ‹

This one fires whenever a datagram comes in from the remote peer. Main character moment ๐Ÿ’–

  • data: bytes โ€” the data that was received โœจ
  • stream_id: int โ€” the ID of the stream the data was received for

class aioquic.h3.events.DataReceived(data, stream_id, stream_ended, push_id=None) ๐ŸŒˆ

The DataReceived event fires whenever data arrives on a stream from the remote peer. It's giving "stuff is happening" ๐Ÿ’…

  • data: bytes โ€” the data that was received
  • push_id: Optional[int] = None โ€” the Push ID, or None if this isn't a push ๐Ÿ“
  • stream_ended: bool โ€” whether the STREAM frame had the FIN bit set ๐Ÿซถ
  • stream_id: int โ€” the ID of the stream the data was received for

class aioquic.h3.events.HeadersReceived(headers, stream_id, stream_ended, push_id=None) ๐ŸŽ€

The HeadersReceived event fires whenever headers are received. Headers are literally the vibe check of every request ๐Ÿ”ฅ

  • headers: list[tuple[bytes, bytes]] โ€” the headers ๐Ÿ’œ
  • push_id: Optional[int] = None โ€” the Push ID, or None if this isn't a push
  • stream_ended: bool โ€” whether the STREAM frame had the FIN bit set
  • stream_id: int โ€” the ID of the stream the headers were received for

class aioquic.h3.events.PushPromiseReceived(headers, push_id, stream_id) ๐Ÿงš

Okay, so the docs say this one is the PushedStreamReceived event, which fires whenever a pushed stream has been received from the remote peer. The class is actually called PushPromiseReceived though, so ๐Ÿ™ˆ just know they're the same energy.

  • headers: list[tuple[bytes, bytes]] โ€” the request headers ๐ŸŒธ
  • push_id: int โ€” the Push ID of the push promise
  • stream_id: int โ€” the Stream ID of the stream that the push is related to

class aioquic.h3.events.WebTransportStreamDataReceived(data, stream_id, stream_ended, session_id) ๐Ÿฆ‹

The WebTransportStreamDataReceived event fires whenever data is received for a WebTransport stream. Very iconic, very specific ๐Ÿ’…

  • data: bytes โ€” the data that was received
  • session_id: int โ€” the ID of the session the data was received for
  • stream_ended: bool โ€” whether the STREAM frame had the FIN bit set
  • stream_id: int โ€” the ID of the stream the data was received for

โœจ Exceptions โœจ

class aioquic.h3.exceptions.H3Error ๐Ÿ’€

This is the base class for all HTTP/3 exceptions. If something goes sideways, this is the parent that catches the drama ๐Ÿ˜ญ

class aioquic.h3.exceptions.InvalidStreamTypeError ๐Ÿ™ˆ

An action was attempted on an invalid stream type. Not the move ๐Ÿ’€

class aioquic.h3.exceptions.NoAvailablePushIDError ๐ŸŒธ

This one shows up when there are no available push IDs left, or when push isn't supported by the remote party. No push IDs left, no cap, no vibes ๐Ÿซ 

โœจ ASYNCIO API โœจ

okay bestie, the asyncio API is a high-level QUIC API built on top of https://docs.python.org/3/library/asyncio.html#module-asyncio, which is Python's standard asynchronous I/O framework ๐Ÿ’… if you already know asyncio you're basically already there ๐Ÿซถ

aioquic comes with a few examples you can copy and learn from, including:

  • an HTTP/3 client ๐Ÿฆ‹
  • an HTTP/3 server ๐ŸŒธ

you can browse all of them on GitHub at https://github.com/aiortc/aioquic/tree/main/examples ๐ŸŒˆ


๐ŸŽ€ Client

async with aioquic.asyncio.connect(host, port, *, configuration=None, create_protocol=<class 'aioquic.asyncio.protocol.QuicConnectionProtocol'>, session_ticket_handler=None, stream_handler=None, token_handler=None, wait_connected=True, local_port=0)

this one connects to a QUIC server at the host and port you give it โœจ connect() returns an awaitable, and when you await it you get a QuicConnectionProtocol back, which is what you use to create streams. the return type is AsyncGenerator[QuicConnectionProtocol, None].

connect() also takes these optional args, so pick whatever your vibe needs:

  • configuration โ€” a QuicConfiguration configuration object (aioquic.quic.configuration.QuicConfiguration) ๐Ÿ’–
  • create_protocol โ€” lets you customize the Protocol (https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol) that manages the connection ๐Ÿงš it should be a callable or class that takes the same arguments as QuicConnectionProtocol and returns a QuicConnectionProtocol instance or a subclass of one.
  • session_ticket_handler โ€” a callback the TLS engine calls when it receives a new session ticket ๐ŸŽซ
  • stream_handler โ€” a callback that runs whenever a stream gets created ๐ŸŒŠ it has to accept two arguments: an asyncio.StreamReader (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader) and an asyncio.StreamWriter (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter).
  • wait_connected โ€” decides whether the context manager waits for the connection to be established before it yields the QuicConnectionProtocol. it defaults to True, but you can set it to False if you want to start sending data right away using 0-RTT ๐Ÿš€
  • local_port โ€” the UDP port number this client wants to bind ๐Ÿ”Œ

๐Ÿ‘‘ Server

await aioquic.asyncio.serve(host, port, *, configuration, create_protocol=<class 'aioquic.asyncio.protocol.QuicConnectionProtocol'>, session_ticket_fetcher=None, session_ticket_handler=None, retry=False, stream_handler=None)

this starts a QUIC server at the host and port you pick ๐Ÿฐ serve() requires a QuicConfiguration that has a TLS certificate and private key, passed in as the configuration argument. the return type is QuicServer.

serve() also takes these optional args:

  • create_protocol โ€” same deal as the client version: customize the Protocol (https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol) that manages each connection. it should be a callable or class that takes the same arguments as QuicConnectionProtocol and returns a QuicConnectionProtocol instance or a subclass.
  • session_ticket_fetcher โ€” a callback the TLS engine calls when the peer presents a session ticket ๐Ÿ” it should return the session ticket with the matching ID, or None if it can't find one.
  • session_ticket_handler โ€” a callback the TLS engine calls when a new session ticket gets issued ๐Ÿ“ฆ it should store the ticket so it can be looked up later.
  • retry โ€” decides whether client addresses get validated before the cryptographic handshake, using a retry packet ๐Ÿ›ก๏ธ it's False by default.
  • stream_handler โ€” a callback that runs whenever a stream is created ๐ŸŒŠ it has to accept two arguments: an asyncio.StreamReader (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader) and an asyncio.StreamWriter (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter).

๐ŸŒˆ Common

class aioquic.asyncio.QuicConnectionProtocol(quic, stream_handler=None)

this is the protocol class both sides use to manage a connection, and these are the methods you'll be calling on it ๐Ÿง 

change_connection_id()

swaps out the connection ID used to talk to the peer ๐Ÿ”„ the old connection ID gets retired. the return type is None.

close(error_code=QuicErrorCode.NO_ERROR, reason_phrase='')

closes the connection ๐Ÿ‘‹ the parameters are:

  • error_code (int) โ€” an error code that explains why the connection is closing ๐Ÿšจ
  • reason_phrase (str) โ€” a human-readable explanation of why the connection is closing ๐Ÿ’ฌ

the return type is None.

connect(addr, transmit=True)

kicks off the TLS handshake ๐Ÿค this can only be called by clients, and only a single time. the return type is None.

await create_stream(is_unidirectional=False)

creates a QUIC stream and gives you back a (reader, writer) pair ๐ŸŽ€ the reader and writer are instances of asyncio.StreamReader (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader) and asyncio.StreamWriter (https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter). the return type is tuple[StreamReader, StreamWriter].

await ping()

pings the peer and waits for the response ๐Ÿ“ the return type is None.

quic_event_received(event)

gets called whenever a QUIC event comes in ๐Ÿ“จ if you're subclassing, reimplement this to handle the events. the return type is None.

request_key_update()

asks for an update of the encryption keys ๐Ÿ” the return type is None.

transmit()

sends any pending datagrams to the peer and arms the timer if it's needed โฐ this gets called automatically when data comes in from the peer or when a timer goes off. but if you poke at the underlying QuicConnection (aioquic.quic.connection.QuicConnection) directly, make sure you call this method whenever data needs to head out to the network. the return type is None.

await wait_closed()

waits for the connection to close ๐Ÿฅ€ the return type is None.

await wait_connected()

waits for the TLS handshake to finish โœ… the return type is None.

โœจ CHANGELOG โœจ

okay so this is the bit where the devs spill the tea on every release ๐Ÿ’… we're going newest to oldest, bestie ๐Ÿซถ

๐ŸŽ€ 1.3.0

  • Ensure PATH_CHALLENGE frames get sent before any other frame types ๐Ÿฆ‹
  • Reclaim memory when HTTP/3 streams end ๐Ÿงนโœจ
  • Limit the number of remote PATH_CHALLENGE stored per path ๐Ÿ”’
  • Avoid an assertion error when receiving multiple STOP_SENDING frames ๐Ÿ™ˆ
  • Improve type annotations ๐Ÿ’œ
  • Make it possible to use LibreSSL instead of OpenSSL ๐ŸŒธ
  • Add support for Python 3.13 and 3.14, and drop support for Python 3.8 and 3.9 ๐Ÿ‘‘
  • Build binary wheels for musllinux <https://peps.python.org/pep-0656/> ๐ŸŒˆ
  • Build binary wheels against OpenSSL <https://www.openssl.org/> 3.5.4 ๐Ÿ”ฅ
  • Add a command-line argument to the http3_client example to request a key update during interoperability tests ๐Ÿงš

๐ŸŽ€ 1.2.0

  • Add support for compatible version handling as defined in RFC 9368 <https://datatracker.ietf.org/doc/html/rfc9368.html> ๐Ÿฆ‹
  • Add support for QUIC Version 2, as defined in RFC 9369 <https://datatracker.ietf.org/doc/html/rfc9369.html> ๐Ÿ‘‘
  • Drop support for draft QUIC versions which were obsoleted by RFC 9000 <https://datatracker.ietf.org/doc/html/rfc9000.html> ๐Ÿ’€
  • Improve datagram padding to allow better packet coalescing and reduce the number of roundtrips during connection establishement โญ
  • Fix server anti-amplification checks during address validation so they take invalid packets, such as datagram-level padding, into account ๐Ÿซถ
  • Allow asyncio clients to make efficient use of 0-RTT by passing wait_connected=False to connect() <#aioquic.asyncio.connect> ๐Ÿš€
  • Add command-line arguments to the http3_client example for client certificates and for negotiating QUIC Version 2 ๐Ÿ“

๐ŸŽ€ 1.1.0

๐ŸŽ€ 1.0.0

  • Ensure no data is sent after a stream reset ๐Ÿ™…โ€โ™€๏ธ
  • Make H3Connection <#aioquic.h3.connection.H3Connection>'s send_datagram() <#aioquic.h3.connection.H3Connection.send_datagram> and send_push_promise() <#aioquic.h3.connection.H3Connection.send_push_promise> methods raise an InvalidStreamTypeError <#aioquic.h3.exceptions.InvalidStreamTypeError> exception if an invalid stream ID is specified ๐Ÿšจ
  • Improve the documentation for QuicConnectionProtocol <#aioquic.asyncio.QuicConnectionProtocol>'s transmit() <#aioquic.asyncio.QuicConnectionProtocol.transmit> method ๐Ÿ“š
  • Fix utcnow() <https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow> deprecation warning on Python 3.12 by using cryptography <https://cryptography.io/> 42.0 and timezone-aware datetime <https://docs.python.org/3/library/datetime.html#datetime.datetime> instances when validating TLS certificates ๐Ÿงผ
  • Build binary wheels against OpenSSL <https://www.openssl.org/> 3.2.0 ๐Ÿ“ฆ
  • Ignore any non-ASCII ALPN values received ๐Ÿ™ˆ
  • Perform more extensive HTTP/3 header validation in H3Connection <#aioquic.h3.connection.H3Connection> ๐Ÿ”
  • Fix exceptions when draining stream writers in the asyncio API <> ๐ŸŒŠ
  • Set the QuicConnection <#aioquic.quic.connection.QuicConnection> idle timer according to RFC 9000 <https://datatracker.ietf.org/doc/html/rfc9000.html> section 10.1 โฐ
  • Implement fairer stream scheduling in QuicConnection <#aioquic.quic.connection.QuicConnection> to avoid head-of-line blocking ๐Ÿšฆ
  • Only load certifi <https://github.com/certifi/python-certifi> root certificates if none was specified in the QuicConfiguration <#aioquic.quic.configuration.QuicConfiguration> ๐ŸŽฏ
  • Improve padding of UDP datagrams containing Initial packets to comply with RFC 9000 <https://datatracker.ietf.org/doc/html/rfc9000.html> section 14.1 ๐Ÿ“
  • Limit the number of pending connection IDs marked for retirement to prevent a possible DoS attack ๐Ÿ›‘

โœจ LICENSE โœจ

okay the legal part, but make it cute ๐ŸŽ€ this is the BSD 3-Clause license, so the vibe is: keep the copyright notice, keep the disclaimer, and don't use the names of the contributors to promote stuff without permission ๐Ÿ™…โ€โ™€๏ธ

Copyright (c) Jeremy Lainรฉ. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of aioquic nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ๐Ÿ’€ translation: no warranties, no promises, nothing. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ๐Ÿ˜ญ


โœจ AUTHOR โœจ

the genius behind it all is Jeremy Lainรฉ ๐Ÿ‘‘๐Ÿซถ

Jeremy Lainรฉ owns the copyright ๐ŸŽ€


Written up on Jan 17, 2026 for AIOQUIC(1) ๐Ÿฆ‹ bestie, that's the whole manual, go touch some grass and then go read the source ๐Ÿ’…โœจ

๐Ÿ“ฆ where this page comes from