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 opcode

View on GitHub (pinned to d9aaf697c2)

Solutions

  1. Handle WSMsgType.ERROR in the receive loop and close cleanly with `await ws.close()`.
  2. If you control the peer, only send continuation frames after a FIN=0 text/binary frame.
  3. 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

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


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