Skip to content

blackbull.websocket

blackbull.websocket

The high-level WebSocket handler object.

A :class:WebSocket wraps the raw (conn, receive, send) triplet so a handler works in data and methods instead of event dicts::

@app.route(path='/chat', scheme=Scheme.websocket)
async def chat(ws: WebSocket):
    await ws.accept()
    async for message in ws:
        await ws.send_text(message)

The equivalent raw handler has to know that the first receive() yields websocket.connect, that accept is a send, that a text message hides under event['text'] while a binary one hides under event['bytes'], and that the loop ends on a websocket.disconnect whose code lives in yet another key. None of that is protocol knowledge — it is transport encoding, which is exactly what a framework should absorb.

The raw triplet form is not deprecated. It stays supported for at least a year (see docs/guide/websockets.md); this object is additive, and both forms run over the same actor, codec, and sender. Nothing about the wire changes — a handler that uses this object produces byte-identical frames to one that sends the dicts by hand.

Naming follows the native-surface rule: this is a native surface, so it is unprefixed and lives outside asgi.py. The ASGIEvent dicts it builds are the boundary representation, and stay there.

WebSocket

One WebSocket connection, as an object.

Constructed by the framework and handed to handlers that ask for it by annotation (ws: WebSocket) or by name (ws / websocket). A handler that takes the raw (conn, receive, send) triplet keeps receiving exactly that, and this object is never built for it.

The handshake is explicit: the connection is not live until :meth:accept returns, and sending before that is an error. To reject a connection, call :meth:close instead of :meth:accept.

accepted property

True once :meth:accept has completed the handshake.

client property

(host, port) of the peer; the port is None on a UDS.

client_disconnected property

True once the peer's close has been observed.

Only ever set by reading — the disconnect arrives on the receive channel, so a handler that has stopped receiving will not see this flip. Use it to break out of a send-only loop that also receives.

close_code property

The close code, once either side has closed; None while open.

connection property

The underlying :class:~blackbull.connection.Connection.

Everything the handshake carried — headers, cookies, TLS state, the client address — is reachable through it. The shortcuts below cover the fields WebSocket handlers actually reach for.

subprotocols property

Subprotocols the client offered, in its order of preference.

__aiter__()

Iterate messages until the peer disconnects.

The disconnect ends the loop rather than raising, so the common shape is just async for message in ws:.

accept(subprotocol=None, *, headers=None) async

Complete the handshake.

subprotocol names the one being accepted from :attr:subprotocols; leaving it None keeps the server's automatic negotiation, exactly as sending {'type': 'websocket.accept', 'subprotocol': None} does on the raw path. headers are extra response headers for the 101.

Raises :class:WebSocketDisconnect if the peer abandoned the handshake before it could be completed.

When middleware already accepted for you (blackbull.middleware.websocket), a bare accept() is a no-op so the same handler body works with or without it. Asking for a specific subprotocol or extra headers in that situation raises instead of silently dropping the request — the 101 has already gone out and cannot carry them.

close(code=_NORMAL_CLOSURE, reason=None) async

Close the connection, or reject the handshake.

Called before :meth:accept, this rejects the connection — the client's connect() fails rather than opening and immediately closing. Idempotent, and a no-op once the peer has already gone, so a finally: await ws.close() is always safe.

receive() async

Await one complete message — str for text, bytes for binary.

Fragmented messages have already been reassembled, so what comes back is always a whole application message.

Raises :class:WebSocketDisconnect when the peer closes. Prefer async for unless you need the close code.

receive_bytes() async

Await one message, requiring it to be binary.

receive_json() async

Await one message and parse it as JSON (text or binary).

receive_text() async

Await one message, requiring it to be text.

send(data) async

Send data as text or binary, chosen by its Python type.

send_bytes(data) async

Send one complete binary message.

send_json(data, *, binary=False) async

JSON-serialise data and send it as one message.

Text by default, which is what browsers and most clients expect; pass binary=True to send the UTF-8 encoding as a binary frame instead.

send_text(data) async

Send one complete text message.

WebSocketDisconnect

Bases: Exception

The peer closed the connection.

Raised by :meth:WebSocket.receive and its typed variants. Iterating with async for handles this for you — the loop simply ends — so catch it only when you call receive() directly and need the close code.

code is the RFC 6455 close code the peer sent, or 1005 when it sent none. reason is its optional UTF-8 explanation.

connect_consumed(conn)

True once the websocket.connect event has been read for conn.

handshake_accepted(conn)

True once the WebSocket handshake has been accepted for conn.

handshake_closed(conn)

True once websocket.close has been sent for conn.

mark_connect_consumed(conn)

Record that websocket.connect has been taken off the channel.

For middleware that pops the connect event without answering it — an auth layer that wants the option to reject with a close code, say. A handler-side :class:WebSocket then knows not to wait for a handshake offer that is already gone, but still sends the accept itself.

mark_handshake_accepted(conn)

Record that websocket.accept has been sent for conn.

Implies :func:connect_consumed — the offer must have been read before it could be answered.

mark_handshake_closed(conn)

Record that websocket.close has been sent for conn.