aio-libs/aiohttp · error · WSMessageTypeError

Received message : is not WSMsgType.TEXT

Error message

Received message {msg.type}:{msg.data!r} is not WSMsgType.TEXT

What it means

Raised by receive_str() when the message returned by receive() has a type other than WSMsgType.TEXT. receive_str() is a convenience that assumes the next frame is text; any other frame type (BINARY, PING, PONG, CLOSE, ERROR, or the synthetic CLOSED/CLOSING messages) trips the guard. WSMessageTypeError subclasses TypeError so it stands apart from transport errors.

Solutions

  1. Use the generic ws.receive() loop and branch on msg.type, only decoding when type is WSMsgType.TEXT.
  2. If the server legitimately sends binary, switch to receive_bytes().
  3. Guard the teardown: stop calling receive_str() after a CLOSE or CLOSED message.
  4. Handle WSMessageTypeError explicitly so control frames do not crash the reader.

Example fix

# before
async for _ in range(10):
    text = await ws.receive_str()
# after
while True:
    msg = await ws.receive()
    if msg.type is WSMsgType.TEXT:
        text = msg.data
    elif msg.type in (WSMsgType.CLOSED, WSMsgType.CLOSE):
        break
Defensive patterns

Strategy: validation

Validate before calling

async def receive_text(ws, timeout=None):
    msg = await ws.receive(timeout)
    if msg.type is aiohttp.WSMsgType.TEXT:
        return msg.data
    if msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE):
        raise ConnectionResetError('websocket closed')
    return None  # control frame; caller decides

Type guard

from aiohttp import WSMsgType

def is_text(msg) -> bool:
    return msg.type is WSMsgType.TEXT

Try / catch

from aiohttp import WSMessageTypeError
try:
    text = await ws.receive_str()
except WSMessageTypeError as exc:
    log.warning('non-text frame ignored: %s', exc)
    text = None

Prevention

When it happens

Trigger: Server sends a BINARY frame while the client loops on receive_str(); server initiates a CLOSE and the client calls receive_str() after; a PING/PONG control frame is the next frame in the queue; an ERROR frame (e.g. decompression failure) is delivered.

Common situations: Protocol mismatch: server treats the channel as binary, client treats it as text; not draining control frames before expecting text; continuing to call receive_str() during teardown.

Related errors


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

Appendix: source

Thrown at aiohttp/client_ws.py:481

    @overload
    async def receive_str(
        self: "ClientWebSocketResponse[Literal[False]]", *, timeout: float | None = None
    ) -> bytes: ...

    @overload
    async def receive_str(
        self: "ClientWebSocketResponse[_DecodeText]", *, timeout: float | None = None
    ) -> str | bytes: ...

    async def receive_str(self, *, timeout: float | None = None) -> str | bytes:
        """Receive TEXT message.

        Returns str when decode_text=True (default), bytes when decode_text=False.
        """
        msg = await self.receive(timeout)
        if msg.type is not WSMsgType.TEXT:
            raise WSMessageTypeError(
                f"Received message {msg.type}:{msg.data!r} is not WSMsgType.TEXT"
            )
        return msg.data

    async def receive_bytes(self, *, timeout: float | None = None) -> bytes:
        msg = await self.receive(timeout)
        if msg.type is not WSMsgType.BINARY:
            raise WSMessageTypeError(
                f"Received message {msg.type}:{msg.data!r} is not WSMsgType.BINARY"
            )
        return msg.data

    @overload
    async def receive_json(
        self: "ClientWebSocketResponse[Literal[True]]",
        *,
        loads: JSONDecoder = ...,
        timeout: float | None = None,

View on GitHub (pinned to d041d4d0fd)