aio-libs/aiohttp · error · HTTPBadRequest

Handshake error

Error message

Handshake error: {key!r}

What it means

Branch 1 of the handshake-key check (web_ws.py:317): if the Sec-WebSocket-Key header is missing/falsy OR its base64-decoded form is not exactly 16 bytes, aiohttp returns HTTP 400. Per RFC 6455 §4.1, the client must send a 16-byte random value base64-encoded in this header. A missing or wrong-length key means the handshake is invalid.

Solutions

  1. On the client, generate a correct key: import os, base64; key = base64.b64encode(os.urandom(16)).decode(). The aiohttp client and browsers do this automatically.
  2. Guard with ws.can_prepare(request) and return a friendly 400 instead of letting the framework's default error render.
  3. If you control the client, prefer aiohttp.ClientSession().ws_connect(url) over a hand-built handshake.

Example fix

// before — hand-built client key
// headers['Sec-WebSocket-Key'] = 'fixed-test-key'  # wrong length
// after
import os, base64
headers['Sec-WebSocket-Key'] = base64.b64encode(os.urandom(16)).decode()
Defensive patterns

Strategy: validation

Validate before calling

import base64, binascii

def valid_ws_key(request) -> bool:
    key = request.headers.get('Sec-WebSocket-Key')
    if not key:
        return False
    try:
        return len(base64.b64decode(key)) == 16
    except binascii.Error:
        return False

if not valid_ws_key(request):
    return web.Response(status=400, text='invalid Sec-WebSocket-Key')

Type guard

import base64, binascii

def is_well_formed_ws_key(key: str | None) -> bool:
    if not isinstance(key, str) or not key:
        return False
    try:
        return len(base64.b64decode(key, validate=True)) == 16
    except (binascii.Error, ValueError):
        return False

Try / catch

ws = web.WebSocketResponse()
try:
    await ws.prepare(request)
except web.HTTPBadRequest as e:
    if 'Handshake error' in (e.text or ''):
        log.warning('client sent malformed Sec-WebSocket-Key')
    return

Prevention

When it happens

Trigger: Client omits Sec-WebSocket-Key; client sends a key that base64-decodes to fewer/more than 16 bytes (e.g. a fixed test string); a proxy that strips or rewrites the header; a malformed probe request.

Common situations: Hand-crafted test requests that reuse a non-16-byte key; bots/scanners probing WS endpoints; intermediaries that rewrite headers; clients built on broken libraries.

Understand the failure class

Related errors


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

Appendix: source

Thrown at aiohttp/web_ws.py:317

            else:
                # No overlap found: Return no protocol as per spec
                ws_logger.warning(
                    "%s: Client protocols %r don’t overlap server-known ones %r",
                    request.remote,
                    req_protocols,
                    self._protocols,
                )

        # check supported version
        version = headers.get(hdrs.SEC_WEBSOCKET_VERSION, "")
        if version not in ("13", "8", "7"):
            raise HTTPBadRequest(text=f"Unsupported version: {version}")

        # check client handshake for validity
        key = headers.get(hdrs.SEC_WEBSOCKET_KEY)
        try:
            if not key or len(base64.b64decode(key)) != 16:
                raise HTTPBadRequest(text=f"Handshake error: {key!r}")
        except binascii.Error:
            raise HTTPBadRequest(text=f"Handshake error: {key!r}") from None

        accept_val = base64.b64encode(
            hashlib.sha1(key.encode() + WS_KEY).digest()
        ).decode()
        response_headers = CIMultiDict(
            {
                hdrs.UPGRADE: "websocket",
                hdrs.CONNECTION: "upgrade",
                hdrs.SEC_WEBSOCKET_ACCEPT: accept_val,
            }
        )

        notakeover = False
        compress = 0
        if self._compress:
            extensions = headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS)

View on GitHub (pinned to d041d4d0fd)