aio-libs/aiohttp · error · HTTPBadRequest
No CONNECTION upgrade hdr
Error message
No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)} What it means
After the Upgrade: websocket header check passes, _handshake() verifies request._message.upgrade is True — i.e. that the connection was actually marked as an upgrade by the HTTP parser (driven by the Connection: upgrade header). If the parser did not set the upgrade flag, the request returns HTTP 400. This catches clients that send Upgrade: websocket but omit or mangle the Connection header.
Solutions
- Configure the proxy to pass hop-by-hop headers: nginx proxy_set_header Connection $http_connection;.
- On the client, ensure both 'Upgrade: websocket' and 'Connection: Upgrade' are sent (the aiohttp client does this automatically for ws_connect).
- Guard the handler with ws.can_prepare(request) and return a 400-friendly response instead of crashing.
Example fix
// nginx config — before
location /ws { proxy_pass http://app; }
// after
location /ws {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
} Defensive patterns
Strategy: validation
Validate before calling
def is_valid_ws_request(request) -> bool:
h = request.headers
return (
h.get('Upgrade', '').lower().strip() == 'websocket'
and 'upgrade' in h.get('Connection', '').lower()
)
if not is_valid_ws_request(request):
return web.Response(status=400, text='invalid WS handshake') Type guard
def connection_header_allows_upgrade(request) -> bool:
return 'upgrade' in request.headers.get('Connection', '').lower() Try / catch
ws = web.WebSocketResponse()
try:
await ws.prepare(request)
except web.HTTPBadRequest as e:
log.warning('rejected WS handshake: %s', e.text)
return # response already prepared by the exception Prevention
- Configure proxies to pass Connection and Upgrade headers through (proxy_http_version 1.1).
- Use a real WS client (aiohttp ClientSession.ws_connect, browsers) that sends both headers.
- Branch on ws.can_prepare() so a non-WS request gets a normal HTTP response.
When it happens
Trigger: Client sends 'Upgrade: websocket' but no 'Connection: upgrade'; client sends 'Connection: keep-alive, upgrade' that the parser rejects; a proxy rewrites the Connection header; an old HTTP/1.0 client that doesn't support upgrade.
Common situations: Reverse proxies (nginx, HAProxy) that don't forward the Connection header or set it to 'close'; hand-rolled clients that forget the Connection header; HTTP/1.0 intermediaries that strip hop-by-hop headers.
Related errors
- No WebSocket 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/276506429232aa7c.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_ws.py:283
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(",")
]
for proto in req_protocols:
if proto in self._protocols:
protocol = proto
break
else:
# No overlap found: Return no protocol as per spec
ws_logger.warning(View on GitHub (pinned to d041d4d0fd)