wsproto: Pure-Python WebSocket Protocol Implementation

raw JSON →
1.3.2 verified Tue May 12 auth: no python install: verified quickstart: stale

wsproto is a pure-Python, sans-I/O implementation of the WebSocket protocol stack (RFC 6455) and its per-message compression extension (RFC 7692). It provides a low-level, state-machine-driven API, allowing developers to embed WebSocket communication into various programming paradigms without dictating network or concurrency models. The current version is 1.3.2, with releases occurring on an irregular but active basis as needed by the `python-hyper` community.

pip install wsproto
error ModuleNotFoundError: No module named 'wsproto'
cause The 'wsproto' library is not installed in your current Python environment.
fix
pip install wsproto
error AttributeError: 'Connection' object has no attribute 'send_message'
cause Developers often expect a high-level 'send_message' method, but 'wsproto' provides a low-level 'send' method for sending individual WebSocket frames.
fix
Use the connection.send() method with raw bytes and an appropriate WebSocket opcode (e.g., Opcode.TEXT, Opcode.BINARY).
error wsproto.utilities.InvalidStateError
cause An operation was attempted on the `wsproto.Connection` object when it was not in the correct state to perform that action (e.g., calling `accept()` on an already accepted connection).
fix
Implement proper state handling by checking connection.state before attempting operations, ensuring they align with the connection's current lifecycle stage.
error wsproto.utilities.ProtocolError
cause The `wsproto` connection received bytes that do not conform to the WebSocket protocol specification (e.g., malformed frame header, invalid opcode, or reserved bits set incorrectly).
fix
Verify that the data being sent to connection.receive_data() is valid WebSocket protocol data, debugging the sender or network for corruption or non-compliance.
gotcha wsproto is a 'sans-I/O' library. It manages the WebSocket protocol state but does not perform any network I/O itself. Users must implement their own 'network glue' to send and receive bytes over the actual network (e.g., using `socket` or an async I/O library) and feed them into `wsproto`.
fix Be prepared to manage your own network sockets and I/O loops. `wsproto.WSConnection.send()` returns bytes to transmit, and `wsproto.WSConnection.receive_data(data)` takes bytes received from the network.
gotcha When the underlying network connection drops unexpectedly (e.g., `socket.recv()` returns zero bytes), you must call `ws.receive_data(None)` to inform `wsproto` of the connection closure and update its internal state. Failing to do so can leave the connection in an inconsistent state.
fix Upon detecting a connection drop (e.g., `recv()` returning `b''`), call `ws.receive_data(None)` immediately before tearing down the connection.
gotcha For correct protocol behavior, both client and server connections *must* respond to certain control frames. Specifically, a received `Ping` event requires sending a `Pong` event (use `event.response()`), and a received `CloseConnection` event requires sending a `CloseConnection` event back (also `event.response()`).
fix Always iterate `ws.events()` after receiving data and ensure you handle `Ping` and `CloseConnection` events by sending their respective responses using `ws.send(event.response())`.
gotcha WebSocket data messages (TextMessage, BinaryMessage) can be fragmented across multiple `Message` events. The `data` field of these events represents only a chunk of the message. Applications need to buffer and reassemble these chunks until `event.message_finished` is `True` to get the complete logical message.
fix Maintain a buffer for incoming message data and append `event.data` until `event.message_finished` indicates the full message has arrived. Then, process the complete buffered message.
breaking The primary `WSConnection` class was moved from `wsproto.connection.WSConnection` to `wsproto.WSConnection` in version 0.13.0. Additionally, the method to feed data into the connection was renamed from `receive_bytes` to `receive_data` in the same version.
fix Update imports from `from wsproto.connection import WSConnection` to `from wsproto import WSConnection`. Replace calls to `ws.receive_bytes(data)` with `ws.receive_data(data)`.
gotcha When processing `wsproto` events, `event.data` for `TextMessage` events is a Unicode string (`str`), while for `BinaryMessage` events it is bytes (`bytes`). Attempting to call `.decode()` on `event.data` from a `TextMessage` will result in an `AttributeError` because strings do not have a `.decode()` method.
fix Check the type of the event (e.g., `isinstance(event, wsproto.events.TextMessage)`) before processing its data. If `event` is a `TextMessage`, `event.data` is already a string and should be used directly. If `event` is a `BinaryMessage`, `event.data` is bytes and can be decoded if a string representation is needed.
gotcha When processing `Message` events (like `TextMessage` or `BinaryMessage`), be aware of the type of `event.data`. For `TextMessage` events, `event.data` is a `str`. For `BinaryMessage` events, `event.data` is `bytes`. Attempting to `decode()` a `str` or `encode()` bytes unnecessarily will result in an `AttributeError` or `TypeError`.
fix For `TextMessage` events, `event.data` is already a string; use it directly or `encode()` it to bytes if the target API expects bytes. For `BinaryMessage` events, `event.data` is bytes; use it directly or `decode()` it to a string if the target API expects a string (specifying encoding).
python os / libc status wheel install import disk
3.10 alpine (musl) wheel - 0.10s 18.2M
3.10 alpine (musl) - - 0.12s 18.2M
3.10 slim (glibc) wheel 1.5s 0.07s 19M
3.10 slim (glibc) - - 0.07s 19M
3.11 alpine (musl) wheel - 0.13s 20.1M
3.11 alpine (musl) - - 0.15s 20.1M
3.11 slim (glibc) wheel 1.7s 0.13s 21M
3.11 slim (glibc) - - 0.12s 21M
3.12 alpine (musl) wheel - 0.12s 12.0M
3.12 alpine (musl) - - 0.14s 12.0M
3.12 slim (glibc) wheel 1.5s 0.12s 12M
3.12 slim (glibc) - - 0.13s 12M
3.13 alpine (musl) wheel - 0.10s 11.7M
3.13 alpine (musl) - - 0.12s 11.6M
3.13 slim (glibc) wheel 1.5s 0.11s 12M
3.13 slim (glibc) - - 0.11s 12M
3.9 alpine (musl) wheel - 0.10s 17.7M
3.9 alpine (musl) - - 0.12s 17.7M
3.9 slim (glibc) wheel 1.8s 0.08s 18M
3.9 slim (glibc) - - 0.10s 18M

wsproto is a 'sans-I/O' library, meaning it handles the protocol state machine but not network communication. The quickstart demonstrates a simulated client-server interaction by manually passing bytes between two `WSConnection` instances. In a real application, `client_ws.send()`'s output would be written to a network socket, and `client_ws.receive_data()` would consume bytes read from that socket. Events are processed by iterating `ws.events()`.

from wsproto import WSConnection, ConnectionType
from wsproto.events import Request, AcceptConnection, TextMessage, CloseConnection
import socket
import os

# Example: Simple WebSocket client interaction (sans-I/O)
# This code demonstrates the wsproto logic; actual network I/O is omitted for brevity.
# In a real application, 'send_bytes_to_network' and 'receive_bytes_from_network'
# would interact with a socket or other I/O primitive.

def simulate_client_server_interaction():
    client_ws = WSConnection(ConnectionType.CLIENT)
    server_ws = WSConnection(ConnectionType.SERVER)

    # Client initiates handshake
    client_handshake_request = client_ws.send(Request(host="example.com", target="/"))
    print(f"Client sends handshake request: {client_handshake_request!r}")

    # Server receives handshake request
    server_ws.receive_data(client_handshake_request)
    for event in server_ws.events():
        if isinstance(event, Request):
            print(f"Server receives client Request: {event}")
            server_handshake_response = server_ws.send(AcceptConnection())
            print(f"Server sends handshake response: {server_handshake_response!r}")
            break

    # Client receives handshake response
    client_ws.receive_data(server_handshake_response)
    for event in client_ws.events():
        if isinstance(event, AcceptConnection):
            print(f"Client receives Server Acceptance: {event}")
            break

    # Client sends a message
    client_message_bytes = client_ws.send(TextMessage(data="Hello from client!"))
    print(f"Client sends message: {client_message_bytes!r}")

    # Server receives the message
    server_ws.receive_data(client_message_bytes)
    for event in server_ws.events():
        if isinstance(event, TextMessage):
            print(f"Server receives message: {event.data!r}")
            if event.message_finished:
                # Server echoes back
                server_response_bytes = server_ws.send(TextMessage(data=f"Echo: {event.data.decode()}"))
                print(f"Server sends echo: {server_response_bytes!r}")
                break

    # Client receives server's echo
    client_ws.receive_data(server_response_bytes)
    for event in client_ws.events():
        if isinstance(event, TextMessage):
            print(f"Client receives echo: {event.data.decode()!r}")
            break

    # Client initiates close
    client_close_bytes = client_ws.send(CloseConnection(code=1000, reason="Done"))
    print(f"Client sends close frame: {client_close_bytes!r}")

    # Server receives close
    server_ws.receive_data(client_close_bytes)
    for event in server_ws.events():
        if isinstance(event, CloseConnection):
            print(f"Server receives CloseConnection: {event}")
            server_close_response = server_ws.send(event.response())
            print(f"Server sends close response: {server_close_response!r}")
            break

    # Client receives server's close response
    client_ws.receive_data(server_close_response)
    for event in client_ws.events():
        if isinstance(event, CloseConnection):
            print(f"Client receives CloseConnection response: {event}")
            break

simulate_client_server_interaction()