|
| 1 | +# WebSockets |
| 2 | + |
| 3 | +[WebSockets][mdn] 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()`. |
| 4 | + |
| 5 | +WebSocket support requires the optional `wsproto` dependency: |
| 6 | + |
| 7 | +```shell |
| 8 | +pip install 'httpx2[ws]' |
| 9 | +``` |
| 10 | + |
| 11 | +## Opening a session |
| 12 | + |
| 13 | +`client.websocket()` is a context manager that performs the WebSocket handshake and yields a `WebSocketSession`: |
| 14 | + |
| 15 | +```pycon |
| 16 | +>>> with httpx2.Client() as client: |
| 17 | +... with client.websocket("wss://example.com/ws") as ws: |
| 18 | +... ws.send_text("Hello!") |
| 19 | +... message = ws.receive_text() |
| 20 | +``` |
| 21 | + |
| 22 | +It works the same way with the async client, using `async with` and `await`: |
| 23 | + |
| 24 | +```pycon |
| 25 | +>>> async with httpx2.AsyncClient() as client: |
| 26 | +... async with client.websocket("wss://example.com/ws") as ws: |
| 27 | +... await ws.send_text("Hello!") |
| 28 | +... message = await ws.receive_text() |
| 29 | +``` |
| 30 | + |
| 31 | +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. |
| 32 | + |
| 33 | +The session is closed automatically when exiting the context manager. |
| 34 | + |
| 35 | +For quick one-off sessions, there is also a top-level `httpx2.websocket()` function, mirroring `httpx2.request()`: |
| 36 | + |
| 37 | +```pycon |
| 38 | +>>> with httpx2.websocket("wss://example.com/ws") as ws: |
| 39 | +... ws.send_text("Hello!") |
| 40 | +``` |
| 41 | + |
| 42 | +## Sending and receiving messages |
| 43 | + |
| 44 | +The session provides `send` and `receive` methods for text, bytes, and JSON messages: |
| 45 | + |
| 46 | +```pycon |
| 47 | +>>> ws.send_text("Hello!") |
| 48 | +>>> ws.send_bytes(b"Hello!") |
| 49 | +>>> ws.send_json({"message": "Hello!"}) |
| 50 | +``` |
| 51 | + |
| 52 | +```pycon |
| 53 | +>>> ws.receive_text() |
| 54 | +'Hello!' |
| 55 | +>>> ws.receive_bytes() |
| 56 | +b'Hello!' |
| 57 | +>>> ws.receive_json() |
| 58 | +{'message': 'Hello!'} |
| 59 | +``` |
| 60 | + |
| 61 | +JSON messages are sent as text frames by default. Pass `mode="binary"` to `send_json()` or `receive_json()` to use binary frames instead. |
| 62 | + |
| 63 | +The `receive` methods block until a message is available. Pass a `timeout` in seconds to bound the wait: |
| 64 | + |
| 65 | +```pycon |
| 66 | +>>> ws.receive_text(timeout=2.0) |
| 67 | +``` |
| 68 | + |
| 69 | +If no message arrives in time, the sync session raises `queue.Empty`, while the async session raises `TimeoutError`. |
| 70 | + |
| 71 | +If the received message doesn't match the expected type, `WebSocketInvalidTypeReceived` is raised. |
| 72 | + |
| 73 | +## Subprotocols |
| 74 | + |
| 75 | +To negotiate a subprotocol with the server, pass the `subprotocols` argument. The subprotocol accepted by the server is available as `ws.subprotocol`: |
| 76 | + |
| 77 | +```pycon |
| 78 | +>>> with client.websocket("wss://example.com/ws", subprotocols=["graphql-ws"]) as ws: |
| 79 | +... ws.subprotocol |
| 80 | +'graphql-ws' |
| 81 | +``` |
| 82 | + |
| 83 | +## Keepalive pings |
| 84 | + |
| 85 | +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. |
| 86 | + |
| 87 | +You can also send a Ping manually. `ping()` returns an event that is set when the corresponding Pong is received: |
| 88 | + |
| 89 | +```pycon |
| 90 | +>>> pong = ws.ping() |
| 91 | +>>> pong.wait() # Blocks until the server answers. |
| 92 | +``` |
| 93 | + |
| 94 | +## Configuration |
| 95 | + |
| 96 | +`client.websocket()` and `httpx2.websocket()` accept the following keyword arguments to tune the session behaviour: |
| 97 | + |
| 98 | +| Argument | Default | Description | |
| 99 | +| -------- | ------- | ----------- | |
| 100 | +| `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. | |
| 101 | +| `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. | |
| 102 | +| `keepalive_ping_interval_seconds` | 20.0 | The interval between automatic keepalive pings. Use `None` to disable them. | |
| 103 | +| `keepalive_ping_timeout_seconds` | 20.0 | How long to wait for the server to answer a keepalive ping before considering the connection lost. | |
| 104 | + |
| 105 | +## Accessing the handshake response |
| 106 | + |
| 107 | +The [`Response`](api.md#response) that upgraded the connection is available as `ws.response`, which is useful for inspecting the handshake headers: |
| 108 | + |
| 109 | +```pycon |
| 110 | +>>> with client.websocket("wss://example.com/ws") as ws: |
| 111 | +... ws.response.headers |
| 112 | +``` |
| 113 | + |
| 114 | +## Error handling |
| 115 | + |
| 116 | +WebSocket exceptions live in the `httpx2.websockets` namespace: |
| 117 | + |
| 118 | +```pycon |
| 119 | +>>> from httpx2.websockets import WebSocketDisconnect |
| 120 | +``` |
| 121 | + |
| 122 | +* `WebSocketUpgradeError` - the server didn't accept the upgrade, and responded with something other than a `101 Switching Protocols` status code. The response is available as `.response`. |
| 123 | +* `WebSocketDisconnect` - the server closed the session. The close code and reason are available as `.code` and `.reason`. |
| 124 | +* `WebSocketInvalidTypeReceived` - the received message didn't match the expected type. |
| 125 | +* `WebSocketNetworkError` - a network error occurred, typically because the underlying stream closed or timed out. |
| 126 | + |
| 127 | +All of these are subclasses of `httpx2.websockets.HTTPXWSException`: |
| 128 | + |
| 129 | +```pycon |
| 130 | +>>> from httpx2.websockets import WebSocketDisconnect |
| 131 | +>>> with client.websocket("wss://example.com/ws") as ws: |
| 132 | +... try: |
| 133 | +... message = ws.receive_text() |
| 134 | +... except WebSocketDisconnect as exc: |
| 135 | +... print(f"Connection closed: {exc.code} {exc.reason}") |
| 136 | +``` |
| 137 | + |
| 138 | +## Testing ASGI applications |
| 139 | + |
| 140 | +Just as [`ASGITransport`](advanced/transports.md#asgi-transport) 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: |
| 141 | + |
| 142 | +```python |
| 143 | +from httpx2 import AsyncClient |
| 144 | +from httpx2.websockets import ASGIWebSocketTransport |
| 145 | + |
| 146 | +async with AsyncClient(transport=ASGIWebSocketTransport(app)) as client: |
| 147 | + async with client.websocket("ws://testserver/ws") as ws: |
| 148 | + await ws.send_text("Hello!") |
| 149 | + message = await ws.receive_text() |
| 150 | +``` |
| 151 | + |
| 152 | +Regular HTTP requests through the same transport work too, so a single client can exercise the whole application. |
| 153 | + |
| 154 | +## Acknowledgements |
| 155 | + |
| 156 | +WebSocket support is derived from the excellent [httpx-ws](https://github.com/frankie567/httpx-ws) package by [François Voron](https://github.com/frankie567). |
| 157 | + |
| 158 | +[mdn]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API |
0 commit comments