aio-libs/aiohttp · error · HTTPBadRequest
No WebSocket UPGRADE hdr
Error message
No WebSocket UPGRADE hdr: {headers.get(hdrs.UPGRADE)}\n Can "Upgrade" only to "WebSocket". What it means
During the WebSocket handshake, _handshake() verifies the HTTP UPGRADE header equals 'websocket' (case-insensitive, trimmed). If a non-WebSocket request reaches a WebSocketResponse.prepare() (or the route handler invokes the WS handshake on a regular HTTP request), aiohttp returns HTTP 400 with this message. The handshake is invoked from prepare() via _pre_start, so the error surfaces as an HTTPException that aiohttp renders as a 400 response.
Solutions
- Call ws.can_prepare(request) first and branch: if not ws.can_prepare(request).ok: return web.Response(...).
- Ensure proxies forward Upgrade/Connection ('Connection' header passthrough) — e.g. nginx: proxy_set_header Upgrade $http_upgrade;.
- Point the client at ws:// or wss:// URLs so it sends the correct Upgrade header.
Example fix
// before
ws = web.WebSocketResponse()
await ws.prepare(request) # 400 if not a WS upgrade
// after
ws = web.WebSocketResponse()
if not ws.can_prepare(request).ok:
return web.Response(status=400, text='WebSocket connection required')
await ws.prepare(request) Defensive patterns
Strategy: validation
Validate before calling
async def ws_handler(request):
ws = web.WebSocketResponse()
if not ws.can_prepare(request).ok:
return web.Response(status=400, text='WebSocket upgrade required')
await ws.prepare(request)
# ... ws loop ... Type guard
def is_websocket_upgrade(request) -> bool:
return request.headers.get('Upgrade', '').lower().strip() == 'websocket' Try / catch
ws = web.WebSocketResponse()
try:
await ws.prepare(request)
except web.HTTPBadRequest as e:
# client didn't send a valid WS upgrade
return web.Response(status=400, text='upgrade required') Prevention
- Call ws.can_prepare(request) and branch before ws.prepare().
- Ensure reverse proxies forward the Upgrade header.
- Point clients at ws:// / wss:// URLs.
When it happens
Trigger: A browser issues a normal GET to a path whose handler unconditionally calls await ws.prepare(request); a reverse proxy strips the Upgrade header; the client used a different upgrade token (e.g. 'h2c' for HTTP/2 upgrade); a curl request without -H 'Upgrade: websocket'.
Common situations: Sharing one route between WS and HTTP without checking; proxies/load balancers (nginx) not forwarding Upgrade; clients connecting with the wrong library or a plain HTTP request; WebSocket endpoint hit by a health-check probe.
Related errors
- No CONNECTION upgrade hdr
- Extension for deflate not supported
- Handshake error
- Invalid challenge response
- Invalid connection header
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/ebd2ee51dcda576b.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_ws.py:275
async def prepare(self, request: BaseRequest) -> AbstractStreamWriter:
# make pre-check to don't hide it by do_handshake() exceptions
if self._payload_writer is not None:
return self._payload_writer
protocol, writer = self._pre_start(request)
payload_writer = await super().prepare(request)
assert payload_writer is not None
self._post_start(request, protocol, writer)
await payload_writer.drain()
return payload_writer
def _handshake(
self, request: BaseRequest
) -> tuple["CIMultiDict[str]", str | None, int, bool]:
headers = request.headers
if "websocket" != headers.get(hdrs.UPGRADE, "").lower().strip():
raise HTTPBadRequest(
text=(
f"No WebSocket UPGRADE hdr: {headers.get(hdrs.UPGRADE)}\n Can "
'"Upgrade" only to "WebSocket".'
)
)
if not request._message.upgrade:
raise HTTPBadRequest(
text=f"No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)}"
)
# find common sub-protocol between client and server
protocol: str | None = None
if hdrs.SEC_WEBSOCKET_PROTOCOL in headers:
req_protocols = [
str(proto.strip())
for proto in headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",")
]View on GitHub (pinned to d041d4d0fd)