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

  1. Configure the proxy to pass hop-by-hop headers: nginx proxy_set_header Connection $http_connection;.
  2. On the client, ensure both 'Upgrade: websocket' and 'Connection: Upgrade' are sent (the aiohttp client does this automatically for ws_connect).
  3. 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

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


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)