aio-libs/aiohttp · error · WebSocketError

WSCloseCode.PROTOCOL_ERROR

WSCloseCode.PROTOCOL_ERROR

Error message

Continuation frame for non started message

What it means

Raised as WebSocketError(code=PROTOCOL_ERROR) in WebSocketReader._handle_frame at reader_py.py:205 when a CONTINUATION opcode (0x0) frame arrives but self._opcode is OP_CODE_NOT_SET, i.e. there is no in-progress fragmented text/binary message to continue. Per RFC 6455 a continuation frame must follow a non-final (FIN=0) text/binary frame.

Source

Thrown at aiohttp/_websocket/reader_py.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. Inspect `msg.type == aiohttp.WSMsgType.ERROR` and `msg.data.code` in your receive loop and close the connection cleanly with `await ws.close()`.
  2. If you control the peer, ensure continuation frames are only sent after a FIN=0 text/binary frame.
  3. Reconnect / re-establish the WebSocket session after a PROTOCOL_ERROR, since the stream is no longer trustworthy.

Example fix

// before
async for msg in ws:
    process(msg.data)

# after
async for msg in ws:
    if msg.type == aiohttp.WSMsgType.ERROR:
        log.warning("ws protocol error: %r", msg.data)
        await ws.close()
        break
    process(msg.data)
Defensive patterns

Strategy: try-catch

Try / catch

msg = await ws.receive()
if msg.type == aiohttp.WSMsgType.ERROR:
    err = msg.data  # WebSocketError with .code == WSCloseCode.PROTOCOL_ERROR
    await ws.close()
    return

Prevention

When it happens

Trigger: The remote peer sends a frame with opcode 0x0 (continuation) as its very first data frame, or after a previous fragmented message was already completed. Surfaces to the aiohttp user via `await ws.receive()` returning a message with type WSMsgType.ERROR (data is the WebSocketError) and triggers a close with code 1002.

Common situations: A buggy or non-conformant client/server implementation; a man-in-the-middle or proxy that reorders/drops frames; a fuzz tester; a peer that restarted its frame state machine mid-stream.

Related errors


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