httpx websocket client: httpx-ws (tested)
httpx has no built-in WebSockets; httpx.get("ws://...") just gets 426 Upgrade Required. pip install httpx-ws and use connect_ws(url, client) (or aconnect_ws with AsyncClient).
httpx itself has no WebSocket API. For a WebSocket client you either add the httpx-ws package (it reuses your httpx Client) or use the websockets library directly. Measured with httpx 0.28.1, httpx-ws 0.9.0 and websockets 17.3 on Python 3.13.5; srv is a local websockets echo server that returns its ws:// URL.
1. httpx.get("ws://...") sends a plain HTTP request
import httpx, srv
url = srv.start()
print(url)
r = httpx.get(url)
print(r.status_code, r.reason_phrase, "|", r.text.strip())
Output:
ws://127.0.0.1:8964
426 Upgrade Required | Failed to open a WebSocket connection: invalid Connection header: keep-alive.
You cannot access a WebSocket server directly with a browser. You need a WebSocket client.
httpx accepted the ws:// URL without an error but sent an ordinary GET with no upgrade headers, so the server answered 426 Upgrade Required. A 426 from a WebSocket endpoint means the client did not do the handshake.
2. httpx websocket client with httpx-ws
import httpx, srv
from httpx_ws import connect_ws
url = srv.start().replace("ws://", "http://")
with httpx.Client() as client:
with connect_ws(url, client) as ws:
ws.send_text("hello")
print(ws.receive_text())
Output:
echo: hello
pip install httpx-ws. connect_ws(url, client) performs the upgrade through your existing httpx.Client, so its headers, auth and other client settings apply. Pass the URL with http:// (or https://), as in the block.
3. httpx AsyncClient websocket
import asyncio, httpx, srv
from httpx_ws import aconnect_ws
url = srv.start().replace("ws://", "http://")
async def main():
async with httpx.AsyncClient() as client:
async with aconnect_ws(url, client) as ws:
for i in range(3):
await ws.send_text(f"msg {i}")
print(await ws.receive_text())
asyncio.run(main())
Output:
echo: msg 0
echo: msg 1
echo: msg 2
The async version is aconnect_ws(url, async_client), with await ws.send_text() / await ws.receive_text().
4. WebSocketUpgradeError: the server did not upgrade
import threading, http.server, httpx
from httpx_ws import connect_ws, WebSocketUpgradeError
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a): pass
def do_GET(self):
self.send_response(200); self.send_header("Content-Length", "2"); self.end_headers(); self.wfile.write(b"hi")
s = http.server.ThreadingHTTPServer(("127.0.0.1", 8965), H)
threading.Thread(target=s.serve_forever, daemon=True).start()
with httpx.Client() as client:
try:
with connect_ws("http://127.0.0.1:8965/", client):
pass
except WebSocketUpgradeError as e:
print("WebSocketUpgradeError:", e.response.status_code, "(server did not switch protocols)")
Output:
WebSocketUpgradeError: 200 (server did not switch protocols)
If the endpoint is a normal HTTP route (wrong path, or a proxy that strips Upgrade headers), httpx-ws raises WebSocketUpgradeError with the actual response attached; check e.response.status_code and the URL.
5. Without httpx: the websockets library
import asyncio, srv
from websockets.asyncio.client import connect
url = srv.start()
async def main():
async with connect(url) as ws:
await ws.send("hello")
print(await ws.recv())
asyncio.run(main())
Output:
echo: hello
If you do not need httpx client settings for the handshake, websockets is the standard choice and takes ws:// / wss:// URLs directly.
Related: WebSocket connection failed, WebSocket closed with code 1006, httpx AsyncClient connection pool, httpx vs requests differences.
Frequently asked questions
Does httpx support websockets?
Not by itself. The httpx-ws package adds connect_ws / aconnect_ws on top of an httpx Client or AsyncClient.
Why does httpx return 426 Upgrade Required for a ws:// URL?
httpx 0.28.1 sent a plain GET without the WebSocket handshake headers, so the server refused it with 426.
What does WebSocketUpgradeError mean?
The server answered the handshake with a normal response (e.g. 200 or 404) instead of 101 Switching Protocols.
Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1, httpx-ws 0.9.0 and websockets 17.3 on Python 3.13.5 against a local echo server. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx, httpx-ws and websockets are free.