aio-libs/aiohttp · error · WebSocketError
WSCloseCode.PROTOCOL_ERROR
WSCloseCode.PROTOCOL_ERROR
Error message
Continuation frame for non started message
What it means
Identical logic to error #4 but raised from the compiled Cython reader at reader_c.py:205 (the .pyx-backed WebSocketReader used when C extensions are built). It fires when a CONTINUATION opcode arrives while self._opcode == OP_CODE_NOT_SET (no in-progress fragmented message). The message text, WSCloseCode.PROTOCOL_ERROR, and behaviour are byte-for-byte the same as the pure-Python reader; only the active code path differs based on whether the extension was compiled.
Source
Thrown at aiohttp/_websocket/reader_c.py:205
except Exception as exc:
self._exc = exc
set_exception(self.queue, exc)
return EMPTY_FRAME_ERROR
return EMPTY_FRAME
def _handle_frame(
self,
fin: bool,
opcode: int | cython_int, # Union intended: Cython pxd uses C int
payload: bytes | bytearray,
compressed: int | cython_int, # Union intended: Cython pxd uses C int
) -> None:
msg: WSMessage
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
# Validate continuation frames before processing
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
# load text/binary
if not fin:
# got partial frame payload
if opcode != OP_CODE_CONTINUATION:
self._opcode = opcode
self._partial += payload
return
has_partial = bool(self._partial)
if opcode == OP_CODE_CONTINUATION:
opcode = self._opcode
self._opcode = OP_CODE_NOT_SET
# previous frame was non finished
# we should get continuation opcodeView on GitHub (pinned to d9aaf697c2)
Solutions
- Handle WSMsgType.ERROR in the receive loop and close cleanly with `await ws.close()`.
- If you control the peer, only send continuation frames after a FIN=0 text/binary frame.
- Reconnect after PROTOCOL_ERROR; the stream cannot be recovered.
Example fix
// before
async for msg in ws:
handle(msg.data)
// after
async for msg in ws:
if msg.type == aiohttp.WSMsgType.ERROR:
await ws.close()
break
handle(msg.data) Defensive patterns
Strategy: try-catch
Try / catch
msg = await ws.receive()
if msg.type == aiohttp.WSMsgType.ERROR:
err = msg.data # WebSocketError; .code == WSCloseCode.PROTOCOL_ERROR
await ws.close()
return Prevention
- Always branch on WSMsgType.ERROR; this Cython-path error has identical semantics to the pure-Python one.
- Only emit continuation frames after a FIN=0 data frame if you own the peer.
- Treat PROTOCOL_ERROR as fatal: close and reconnect.
When it happens
Trigger: Same as #4: the remote peer sends a continuation (0x0) frame without a preceding non-final text/binary frame. Whether you hit #4 or #19 depends only on whether aiohttp loaded the Cython reader (default when the extension is built) versus the pure-Python fallback (AIOHTTP_NO_EXTENSIONS=1).
Common situations: A non-conformant or buggy peer sending orphan continuation frames; reproduced consistently because the Cython reader is the default in pip-installed aiohttp; surfaces only as the pure-Python error #4 if extensions are disabled.
Related errors
- WSCloseCode.PROTOCOL_ERROR
- WSCloseCode.INVALID_TEXT
- Invalid upgrade header
- Invalid connection header
- Invalid challenge response
AI-assisted analysis of aio-libs/aiohttp@d9aaf697c2 (2026-08-06).
Data as JSON: /api/errors/f4b68043e0d4bc1a.
Report an issue: GitHub.