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
- Confirm the server implements RFC 6455 Sec-WebSocket-Accept derivation (sha1(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'), base64).
- Inspect the .headers on the caught WSServerHandshakeError to compare the returned accept with the expected one.
- 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
- Use a compliant WebSocket server library for the peer.
- Remove proxies/WAFs that mangle Sec-WebSocket-* headers.
- Add an integration test that asserts the accept hash round-trips.
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 = protoView on GitHub (pinned to d041d4d0fd)