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.