aio-libs/aiohttp · error · WebSocketError
1002
1002
Error message
Continuation frame for non started message
What it means
Raised as a WebSocketError with close code 1002 (PROTOCOL_ERROR) when an inbound WebSocket frame carrying the CONTINUATION opcode (0x0) arrives while the reader has no in-progress fragmented message (self._opcode is still OP_CODE_NOT_SET). RFC 6455 §5.4 requires that a continuation frame always follow a non-final (fin=0) TEXT or BINARY frame; a continuation with no preceding data-frame start is illegal. The reader only sets self._opcode when a non-fin TEXT/BINARY frame is seen, so this fires for a stray or duplicated continuation.
Solutions
- Verify the peer's frame construction: the first frame of every message must use opcode 0x1 (TEXT) or 0x2 (BINARY), and only subsequent fragments use 0x0 (CONTINUATION).
- If you control the sender, fix its fragmentation logic so continuation opcodes are only emitted after a non-fin start frame.
- If the peer is untrusted/legacy, catch WebSocketError in your receive loop, close the connection with the supplied code (1002), and log the offending frame for diagnosis.
- Inspect traffic with a WebSocket-aware debugger (e.g. wireshark) to confirm whether an intermediary is altering opcodes.
Example fix
// before (buggy sender): always sends opcode 0 ws.send_frame(payload, opcode=0x0, fin=False) // after: first fragment uses TEXT/BINARY, later fragments use CONTINUATION ws.send_frame(chunk0, opcode=0x1, fin=False) ws.send_frame(chunk1, opcode=0x0, fin=True)
Defensive patterns
Strategy: try-catch
Validate before calling
// On the receiver, wrap the receive loop; you cannot pre-validate a peer's wire bytes.
try:
msg = await ws.receive()
except WebSocketError as e:
await ws.close(code=e.code if hasattr(e,'code') else 1002)
Type guard
// Identify a stray-continuation protocol error by its code/message.
def is_stray_continuation(err: WebSocketError) -> bool:
return getattr(err, 'code', None) == WSCloseCode.PROTOCOL_ERROR and 'Continuation frame for non started message' in str(err)
Try / catch
try:
async for msg in ws:
handle(msg)
except WebSocketError as exc:
await ws.close(code=getattr(exc, 'code', WSCloseCode.PROTOCOL_ERROR))
log.warning('ws protocol error: %s', exc)
Prevention
- If you control the sender, ensure the first fragment of each message uses TEXT/BINARY and only later fragments use CONTINUATION.
- Always pair the receive loop with a WebSocketError handler that closes with the reported code.
- Validate frames in a test harness (e.g. autobahn testsuite/fuzzingclient) before shipping a custom WebSocket sender.
When it happens
Trigger: A peer (or a misbehaving proxy/intermediary) sends a frame with opcode 0x0 (CONTINUATION) as the first data frame, or sends two consecutive fully-assembled messages where the second incorrectly reuses opcode 0x0. Concretely: _handle_frame is entered with opcode == OP_CODE_CONTINUATION while self._opcode == OP_CODE_NOT_SET (-1).
Common situations: A buggy client/server implementation that always sends opcode 0 for data frames; a man-in-the-middle proxy that rewrites fragment opcodes; fuzz testing; a custom raw-frame sender built by hand that forgets the first fragment must be TEXT/BINARY.
Related errors
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/77659189f302f82e.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/_websocket/reader_py.py:208
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 d041d4d0fd)