aio-libs/aiohttp · error · WebSocketError

1007

1007

Error message

Invalid UTF-8 text message

What it means

Raised as WebSocketError code 1007 (INVALID_TEXT) when a TEXT frame's assembled payload fails UTF-8 decoding (payload_merged.decode('utf-8') raises UnicodeDecodeError). RFC 6455 §8.1 mandates that the connection be closed with code 1007 when a text message contains invalid UTF-8. This path only runs when self._decode_text is True (the default); with decode_text=False the raw bytes are returned and no validation occurs here.

Solutions

  1. Ensure the peer only sends valid UTF-8 in TEXT frames; use BINARY frames (opcode 0x2) for non-text payloads.
  2. If you are the sender, encode with a strict UTF-8 encoder (data.encode('utf-8')) and never split a multibyte character across fragments.
  3. On the receiver side, catch WebSocketError and close with code 1007.
  4. If you intentionally exchange raw bytes, use BINARY frames or set decode_text=False on the reader (Python-only, bypasses validation).

Example fix

// before: sender labels binary as text
ws.send_str(b'\xff\xfe ...')  // not valid utf-8
// after: use bytes via send_bytes / BINARY
ws.send_bytes(raw_payload)
// or ensure utf-8 on the text path
ws.send_str(text.decode('utf-8','replace') if unsure else text)
Defensive patterns

Strategy: try-catch

Validate before calling

// Sender: ensure strict UTF-8 before sending text.
try:
    text.encode('utf-8')  # validates on the send side
except UnicodeEncodeError:
    await ws.send_bytes(payload)  # use BINARY instead

Type guard

def is_valid_utf8(b: bytes) -> bool:
    try:
        b.decode('utf-8'); return True
    except UnicodeDecodeError:
        return False

Try / catch

try:
    msg = await ws.receive()
except WebSocketError as exc:
    if exc.code == WSCloseCode.INVALID_TEXT:
        await ws.close(code=WSCloseCode.INVALID_TEXT)

Prevention

When it happens

Trigger: A peer sends a TEXT frame (opcode 0x1) whose payload is not valid UTF-8 — e.g. a truncated multibyte sequence, lone surrogate bytes, or binary accidentally labelled as text. _handle_frame assembles the payload and calls .decode('utf-8'), which throws.

Common situations: A client serializes binary data into a TEXT frame by mistake; a frame is truncated by a buggy intermediary or network layer cutting a multibyte character; a custom encoder producing non-UTF-8 bytes; partial/fragmented text whose split point lands inside a multibyte sequence at the application layer (the reader reassembles correctly, but a hand-built sender can split badly).

Understand the failure class

Related errors


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

Appendix: source

Thrown at aiohttp/_websocket/reader_py.py:281

                        "Compressed message has too many deflate members",
                    ) from exc
                if self._max_msg_size and len(payload_merged) > self._max_msg_size:
                    raise WebSocketError(
                        WSCloseCode.MESSAGE_TOO_BIG,
                        f"Decompressed message exceeds size limit {self._max_msg_size}",
                    )
            elif type(assembled_payload) is bytes:
                payload_merged = assembled_payload
            else:
                payload_merged = bytes(assembled_payload)

            size = len(payload_merged)
            if opcode == OP_CODE_TEXT:
                if self._decode_text:
                    try:
                        text = payload_merged.decode("utf-8")
                    except UnicodeDecodeError as exc:
                        raise WebSocketError(
                            WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
                        ) from exc

                    # XXX: The Text and Binary messages here can be a performance
                    # bottleneck, so we use tuple.__new__ to improve performance.
                    # This is not type safe, but many tests should fail in
                    # test_client_ws_functional.py if this is wrong.
                    msg = TUPLE_NEW(WSMessageText, (text, size, "", WS_MSG_TYPE_TEXT))
                else:
                    # Return raw bytes for TEXT messages when decode_text=False
                    msg = TUPLE_NEW(
                        WSMessageTextBytes, (payload_merged, size, "", WS_MSG_TYPE_TEXT)
                    )
            else:
                msg = TUPLE_NEW(
                    WSMessageBinary, (payload_merged, size, "", WS_MSG_TYPE_BINARY)
                )

View on GitHub (pinned to d041d4d0fd)