aio-libs/aiohttp · error · WSServerHandshakeError

Invalid challenge response

Error message

Invalid challenge response

What it means

Raised as WSServerHandshakeError when the SEC_WEBSOCKET_ACCEPT response header does not equal base64(sha1(sec_key + WS_KEY)). This proves the server did not follow the RFC 6455 opening-handshake derivation; without it aiohttp cannot trust that the peer actually speaks WebSocket.

Solutions

  1. Confirm the server implements RFC 6455 Sec-WebSocket-Accept derivation (sha1(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'), base64).
  2. Inspect the .headers on the caught WSServerHandshakeError to compare the returned accept with the expected one.
  3. Remove intermediaries that rewrite Sec-WebSocket-* headers, or move to a direct wss:// connection.

Example fix

// before
# server returns Sec-WebSocket-Accept that does not match the derivation
await session.ws_connect('wss://x')  # raises Invalid challenge response
// after
# fix the server to compute base64(sha1(sec_websocket_key + GUID))
Defensive patterns

Strategy: try-catch

Validate before calling

import hashlib, base64

GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

def expected_accept(sec_key: str) -> str:
    return base64.b64encode(hashlib.sha1(sec_key.encode() + GUID).digest()).decode()

# Use to verify the server you are integrating against.

Type guard

import hashlib, base64

def is_valid_accept(sec_key: str, accept_header: str) -> bool:
    expected = base64.b64encode(hashlib.sha1(sec_key.encode() + b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()).decode()
    return accept_header == expected

Try / catch

from aiohttp import WSServerHandshakeError

try:
    ws = await session.ws_connect(url)
except WSServerHandshakeError as e:
    if e.message == 'Invalid challenge response':
        # server did not follow RFC 6455; cannot interoperate
        raise
    raise

Prevention

When it happens

Trigger: Server returns 101 with correct Upgrade/Connection but a wrong or missing Sec-WebSocket-Accept value. r_key != computed match. Happens with non-RFC-compliant servers, some transparent proxies, or when the request's Sec-WebSocket-Key was modified in flight.

Common situations: Custom server that doesn't compute the accept hash. Proxy that rewrites handshake headers. Anti-DDoS/WAF that mangles Sec-WebSocket-* headers. Mismatched WebSocket subprotocol libraries.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/fce7217dcde46ca0. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/client.py:1131

                    message="Invalid upgrade header",
                    status=resp.status,
                    headers=resp.headers,
                )

            if not resp._upgraded:
                raise WSServerHandshakeError(
                    resp.request_info,
                    resp.history,
                    message="Invalid connection header",
                    status=resp.status,
                    headers=resp.headers,
                )

            # key calculation
            r_key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, "")
            match = base64.b64encode(hashlib.sha1(sec_key + WS_KEY).digest()).decode()
            if r_key != match:
                raise WSServerHandshakeError(
                    resp.request_info,
                    resp.history,
                    message="Invalid challenge response",
                    status=resp.status,
                    headers=resp.headers,
                )

            # websocket protocol
            protocol = None
            if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers:
                resp_protocols = [
                    proto.strip()
                    for proto in resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",")
                ]

                for proto in resp_protocols:
                    if proto in protocols:
                        protocol = proto

View on GitHub (pinned to d041d4d0fd)