Skip to content

Commit 95aebc3

Browse files
authored
Document WebSocket support (#1065)
1 parent 2cb02fe commit 95aebc3

4 files changed

Lines changed: 196 additions & 0 deletions

File tree

docs/api.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
::: httpx2.stream
3030

31+
::: httpx2.websocket
32+
3133
## `Client`
3234

3335
::: httpx2.Client
@@ -48,6 +50,7 @@
4850
- query
4951
- stream
5052
- sse
53+
- websocket
5154
- build_request
5255
- send
5356
- close
@@ -72,6 +75,7 @@
7275
- query
7376
- stream
7477
- sse
78+
- websocket
7579
- build_request
7680
- send
7781
- aclose
@@ -221,3 +225,35 @@ what gets sent over the wire.*
221225
- id
222226
- retry
223227
- json
228+
229+
## `WebSocketSession`
230+
231+
::: httpx2.websockets.WebSocketSession
232+
options:
233+
members:
234+
- subprotocol
235+
- response
236+
- send_text
237+
- send_bytes
238+
- send_json
239+
- receive_text
240+
- receive_bytes
241+
- receive_json
242+
- ping
243+
- close
244+
245+
## `AsyncWebSocketSession`
246+
247+
::: httpx2.websockets.AsyncWebSocketSession
248+
options:
249+
members:
250+
- subprotocol
251+
- response
252+
- send_text
253+
- send_bytes
254+
- send_json
255+
- receive_text
256+
- receive_bytes
257+
- receive_json
258+
- ping
259+
- close

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ As well as these optional installs:
115115

116116
* `h2` - HTTP/2 support. *(Optional, with `httpx2[http2]`)*
117117
* `socksio` - SOCKS proxy support. *(Optional, with `httpx2[socks]`)*
118+
* `wsproto` - WebSocket support. *(Optional, with `httpx2[ws]`)*
118119
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
119120
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
120121
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*

docs/websockets.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ nav:
3939
- HTTP/2 Support: 'http2.md'
4040
- Logging: 'logging.md'
4141
- Server-Sent Events: 'sse.md'
42+
- WebSockets: 'websockets.md'
4243
- Requests Compatibility: 'compatibility.md'
4344
- Troubleshooting: 'troubleshooting.md'
4445
- API Reference:

0 commit comments

Comments
 (0)