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

  1. 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).
  2. If you control the sender, fix its fragmentation logic so continuation opcodes are only emitted after a non-fin start frame.
  3. 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.
  4. 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

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 opcode

View on GitHub (pinned to d041d4d0fd)