WebSockets
WebSockets provide a full-duplex communication channel between the client and the server over a single long-lived connection, allowing both sides to send messages at any time. HTTPX has native support for them through client.websocket().
WebSocket support requires the optional wsproto dependency:
pip install 'httpx2[ws]'
Opening a session
client.websocket() is a context manager that performs the WebSocket handshake and yields a WebSocketSession:
>>> with httpx2.Client() as client:
... with client.websocket("wss://example.com/ws") as ws:
... ws.send_text("Hello!")
... message = ws.receive_text()
It works the same way with the async client, using async with and await:
>>> async with httpx2.AsyncClient() as client:
... async with client.websocket("wss://example.com/ws") as ws:
... await ws.send_text("Hello!")
... message = await ws.receive_text()
Both ws:// and wss:// URLs are supported, as well as their http:// and https:// equivalents. The handshake is a regular HTTP request, so the client's configuration - headers, cookies, authentication, proxies, and timeouts - applies as usual.
The session is closed automatically when exiting the context manager.
For quick one-off sessions, there is also a top-level httpx2.websocket() function, mirroring httpx2.request():
>>> with httpx2.websocket("wss://example.com/ws") as ws:
... ws.send_text("Hello!")
Sending and receiving messages
The session provides send and receive methods for text, bytes, and JSON messages:
>>> ws.send_text("Hello!")
>>> ws.send_bytes(b"Hello!")
>>> ws.send_json({"message": "Hello!"})
>>> ws.receive_text()
'Hello!'
>>> ws.receive_bytes()
b'Hello!'
>>> ws.receive_json()
{'message': 'Hello!'}
JSON messages are sent as text frames by default. Pass mode="binary" to send_json() or receive_json() to use binary frames instead.
The receive methods block until a message is available. Pass a timeout in seconds to bound the wait:
>>> ws.receive_text(timeout=2.0)
If no message arrives in time, TimeoutError is raised.
If the received message doesn't match the expected type, WebSocketInvalidTypeReceived is raised.
Subprotocols
To negotiate a subprotocol with the server, pass the subprotocols argument. The subprotocol accepted by the server is available as ws.subprotocol:
>>> with client.websocket("wss://example.com/ws", subprotocols=["graphql-ws"]) as ws:
... ws.subprotocol
'graphql-ws'
Keepalive pings
The session automatically sends a Ping event at a regular interval to keep the connection alive. If the server doesn't answer in time, the connection is considered lost and WebSocketNetworkError is raised.
You can also send a Ping manually. ping() returns an event that is set when the corresponding Pong is received:
>>> pong = ws.ping()
>>> pong.wait() # Blocks until the server answers.
Configuration
client.websocket() and httpx2.websocket() accept the following keyword arguments to tune the session behaviour:
| Argument | Default | Description |
|---|---|---|
max_message_size_bytes |
65536 | The number of bytes read from the network at a time. Larger messages are received in multiple chunks and reassembled. |
queue_size |
512 | The size of the queue holding received messages until they are consumed. When full, the session stops reading from the server until room is available. |
keepalive_ping_interval_seconds |
20.0 | The interval between automatic keepalive pings. Use None to disable them. |
keepalive_ping_timeout_seconds |
20.0 | How long to wait for the server to answer a keepalive ping before considering the connection lost. |
Accessing the handshake response
The Response that upgraded the connection is available as ws.response, which is useful for inspecting the handshake headers:
>>> with client.websocket("wss://example.com/ws") as ws:
... ws.response.headers
Error handling
WebSocket exceptions live in the httpx2.websockets namespace:
>>> from httpx2.websockets import WebSocketDisconnect
WebSocketUpgradeError- the server didn't accept the upgrade, and responded with something other than a101 Switching Protocolsstatus code. The response is available as.response.WebSocketDisconnect- the server closed the session. The close code and reason are available as.codeand.reason.WebSocketInvalidTypeReceived- the received message didn't match the expected type.WebSocketNetworkError- a network error occurred, typically because the underlying stream closed or timed out.
All of these are subclasses of httpx2.websockets.HTTPXWSException:
>>> from httpx2.websockets import WebSocketDisconnect
>>> with client.websocket("wss://example.com/ws") as ws:
... try:
... message = ws.receive_text()
... except WebSocketDisconnect as exc:
... print(f"Connection closed: {exc.code} {exc.reason}")
Testing ASGI applications
Just as ASGITransport lets you make HTTP requests directly against an ASGI application, ASGIWebSocketTransport extends it with WebSocket support - useful for testing Starlette or FastAPI applications without running a server:
from httpx2 import AsyncClient
from httpx2.websockets import ASGIWebSocketTransport
async with AsyncClient(transport=ASGIWebSocketTransport(app)) as client:
async with client.websocket("ws://testserver/ws") as ws:
await ws.send_text("Hello!")
message = await ws.receive_text()
Regular HTTP requests through the same transport work too, so a single client can exercise the whole application.
Acknowledgements
WebSocket support is derived from the excellent httpx-ws package by François Voron.