WebSockets
pip install austial # `austial.websockets` ships in austial-core, which `austial` installsA gateway is a provider that serves a WebSocket endpoint. Clients and the server exchange JSON text frames
{"event": "name", "data": ..., "id": optional}; a handler is picked by event, and what it returns is sent back as
{"event": "name", "data": <return value>} (with the same id, so a client can match replies to requests).
from austial import Injectable, Module
from austial.websockets import (
ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect,
SubscribeMessage, WebSocketGateway, WebSocketsModule, WsClient, WsServer,
)
@WebSocketGateway("/chat")
@Injectable()
class ChatGateway(OnGatewayConnection, OnGatewayDisconnect):
server: WsServer # set by the framework
def __init__(self, messages: MessagesService): # normal dependency injection
self.messages = messages
async def handle_connection(self, client: WsClient) -> None:
await client.send("welcome", {"id": client.id})
def handle_disconnect(self, client: WsClient) -> None: ...
@SubscribeMessage("message")
async def on_message(self, text: str = MessageBody("text"), client: WsClient = ConnectedSocket()):
saved = await self.messages.save(client.id, text)
await self.server.to("lobby").emit("message", saved) # broadcast to a room
return {"delivered": True} # reply to the sender
@Module(imports=[WebSocketsModule.for_root()], providers=[ChatGateway, MessagesService])
class ChatModule:
passGateways are mounted when the application bootstraps. Two gateways on one path fail at startup.
Handler arguments#
| You write | You get |
|---|---|
x = MessageBody() | the message data |
x = MessageBody("key") | data["key"]; extra positional arguments are pipes: MessageBody("n", ParseIntPipe) |
client: WsClient = ConnectedSocket(), or just annotate client: WsClient | the sender |
| a first unmarked parameter | the data |
Replies#
| Return | Sends |
|---|---|
| a value | {"event": <received event>, "data": value} |
WsResponse("other-event", data) | that event name instead |
None | nothing |
raise WsException("message") | {"event": "exception", "data": {"status": "error", "message": "..."}} |
An HttpException raised in a handler is turned into the same exception event; any other exception is logged and answered with
Internal server error (details are never leaked). Malformed frames and unknown events also get an exception event. The
connection stays open.
Clients, rooms and broadcasting#
WsClient has id, send(event, data), await join(room), await leave(room), rooms, close(code), headers, query_params and a free-form
data dict for per-connection state (the authenticated user, …). WsServer has clients, emit(event, data) for everyone and
to(room).emit(...) (chainable: to("a").to("b")). A client’s rooms are left automatically on disconnect.
Guards, pipes and interceptors#
@UseGuards, @UsePipes and @UseInterceptors work on a gateway class or a message handler. Guards get a context whose
get_type() is "ws"; read the sender and the data with context.switch_to_ws():
class WsAuthGuard(CanActivate):
async def can_activate(self, context):
client = context.switch_to_ws().get_client()
return client.query_params.get("token") == "ok" # the upgrade request's query string and headersA denied message is answered with a Forbidden resource exception event. Guards run per message, not at connection time; to
refuse a connection, check in handle_connection and await client.close(4401).
Custom adapters#
WebSocketsModule.for_root(adapter=...) takes a WsAdapter. Subclass it to serve gateways over another protocol: implement
mount(runner, app, path, gateway, handlers) -> WsServer, and for each incoming message call
await runner.process(gateway, handlers[event], client, event, data), which applies guards, pipes and interceptors, maps errors to
exception events, and returns (reply_event, payload) or None. SocketIoAdapter is built this way.
Socket.IO#
By default the server speaks plain WebSocket text frames (use the browser’s WebSocket). To serve the same gateways to
socket.io-client, install pip install 'austial-core[socketio]' and pass an adapter:
from austial.websockets import SocketIoAdapter, WebSocketsModule
@Module(imports=[WebSocketsModule.for_root(adapter=SocketIoAdapter(cors_allowed_origins="https://app.example.com"))],
providers=[ChatGateway])
class ChatModule: ...- The engine is mounted at
/socket.io; a gateway’s path is its Socket.IO namespace (@WebSocketGateway("/chat")isio("/chat")). - Gateway code is unchanged. A handler’s return value is the acknowledgement when the client asked for one;
WsResponseandawait client.send(...)emit events; errors becomeexceptionevents and the connection stays open. client.data["auth"]holds the client’sauthpayload. Reject a connection withawait client.close()inhandle_connection.client.join/client.leaveareasync(await client.join(room)) andserver.to(room).emit(...)maps to Socket.IO rooms.- Extra keyword arguments to
SocketIoAdaptergo tosocketio.AsyncServer. Scaling across processes needs a Socket.IO client manager (Redis, say), passed asclient_manager=.