Use it for
Small WebSocket endpoints that share one message handler and optionally need disconnect cleanup.
Server reference
A small adapter that turns a server route into an aiohttp WebSocket endpoint, handles the PmSocket control protocol, and delegates application messages to one handler.
PmSocket sits on top of the repository's custom server router. A decorated route runs normally first. If that route returns None, PmSocket upgrades the request to an aiohttp.web.WebSocketResponse, sends a connection control message, and begins dispatching incoming frames.
Small WebSocket endpoints that share one message handler and optionally need disconnect cleanup.
It is a pub/sub layer, connection registry, serializer, authentication system, or request/response protocol. Your application supplies those pieces.
None to accept the WebSocket.Create one adapter per endpoint, register its route, then attach an async message handler. The companion browser client is covered in the JavaScript guide.
import aiohttp
import aiohttp_ws
server = aiohttp_ws.Server(host="127.0.0.1", port=8080)
connection_registry = set()
chat_socket = aiohttp_ws.PmSocket(server)
@chat_socket.route("/chat/ws", method="GET")
async def open_chat(request):
# Validate query parameters, authenticate, or create connection state here.
# None tells PmSocket to perform the WebSocket upgrade.
return None
@chat_socket.handler
async def receive_chat_message(ws, msg):
connection_registry.add(ws)
if msg.type is aiohttp.WSMsgType.TEXT and not msg.data.startswith("::"):
await ws.send_str(f"echo:{msg.data}")
@chat_socket.on_disconnect
async def remove_chat_connection(ws):
connection_registry.discard(ws)
server.run()None.route().None value, that value is passed back to the normal HTTP response machinery.None, PmSocket creates WebSocketResponse(heartbeat=5, compress=False) and prepares it.::connect::. The bundled browser client marks itself alive when it receives this frame.handle_msg(), which handles control text and then calls the registered application handler.WebSocketResponse is returned.The server enables an aiohttp heartbeat every five seconds and disables WebSocket compression. These settings are fixed by the current implementation.
PmSocket(server)Creates an adapter attached to the custom server instance. The instance stores one application message handler and one optional disconnect hook.
route(route, method="*", cache_time=None, server_cache_time=None, pass_cookies=False, **kwargs)Registers the decorated function through the server's routing layer. Both synchronous and asynchronous route functions are accepted.
| Parameter | Meaning | Default |
|---|---|---|
route | URL path to register, such as /chat/ws. | Required |
method | One uppercase HTTP method string or "*". The iterable method feature in Server.route() v1.1.5 does not apply to this adapter. | "*" |
cache_time | Browser cache lifetime in seconds, passed to the routing layer. | None |
server_cache_time | Server response-cache lifetime in seconds. Cached response headers are not retained. | None |
pass_cookies | Leave this False: the current PmSocket wrapper accepts only request, so enabling it makes the outer wrapper pass an unsupported second argument. Use Cookies(request) in the route for inspection. | False |
**kwargs | Metadata stored in the route wrapper. Custom application hooks may interpret it; the library does not implement auth or api behavior. | None |
The underlying router supports normal response values including an aiohttp response, HTML text, bytes, JSON dictionaries, and optional status codes. In this wrapper, only a literal None triggers the WebSocket upgrade.
handler(function) / @socketSets the single application handler. The function receives (ws, msg). It may be synchronous or asynchronous. Registering another handler replaces the previous one.
@chat_socket
async def receive(ws, msg):
...
# Equivalent registration style:
@chat_socket.handler
async def receive(ws, msg):
...Both decorator forms return the PmSocket instance, so the decorated function name no longer refers to the original function. Use another name if you need to call that function directly.
on_disconnect(function)Registers one cleanup hook receiving ws. It is always awaited after the WebSocket loop, so define it with async def. Registering another hook replaces the previous one.
await handle_msg(ws, msg)Processes one aiohttp message. This is called by the route loop and normally should not be called by application code. Text control messages are handled first; the same message is then passed to the application handler.
| Text frame | Direction | Server behavior |
|---|---|---|
::connect:: | Server → client | Sent immediately after the upgrade is prepared. |
::ping:: | Either direction | The receiver answers with ::pong::. |
::pong:: | Either direction | Used by the JavaScript client to complete latency measurement. |
::disconnect:: | Client → server | The server closes the WebSocket. |
::ping:: or closes on ::disconnect::, it still invokes the application handler with that frame. Ignore reserved values explicitly if your handler only expects application data.msg argumentmsg is an aiohttp.WSMessage. Inspect msg.type before interpreting msg.data.
| Message type | Typical data | Suggested handling |
|---|---|---|
WSMsgType.TEXT | str | Parse your text or JSON application protocol. |
WSMsgType.BINARY | bytes | Process bytes or reject if unsupported. |
WSMsgType.ERROR | Exception information | Log and allow the connection loop to finish. |
| Close/control types | Type-dependent | Usually ignore unless the application needs telemetry. |
ws argumentws is an aiohttp.web.WebSocketResponse. Common operations include await ws.send_str(text), await ws.send_json(value), await ws.send_bytes(data), and await ws.close(). State is available through properties such as ws.closed and ws.close_code.
Perform authentication in the route or in an application-owned before-request hook. Arbitrary flags such as auth=True do not authenticate a connection. Return an HTTP response on failure and None after validation succeeds. The following fragment assumes the application provides tokens.is_valid().
@chat_socket.route("/chat/ws", method="GET")
async def open_chat(request):
token = request.query.get("token")
if not token or not await tokens.is_valid(token):
return {"error": "unauthorized"}, 401
return Noneimport json
@chat_socket.handler
async def receive(ws, msg):
if msg.type is not aiohttp.WSMsgType.TEXT:
return
if msg.data.startswith("::"):
return
try:
payload = json.loads(msg.data)
except json.JSONDecodeError:
await ws.send_json({"type": "error", "message": "Invalid JSON"})
return
await dispatch(payload, ws)PmSocket does not maintain a client collection. The route receives only request; the socket is created after it returns. Register ws when the first application message arrives, as in the quick start, then remove it in on_disconnect. Make cleanup idempotent because a connection may already have been removed elsewhere.
There are three distinct objects in a PmSocket application: the adapter you construct, the live aiohttp socket created for each accepted request, and each native aiohttp message received on that socket. The adapter is shared between its connections; application state stored directly on it is also shared.
| Object/API | Input and return | Ownership and mutation |
|---|---|---|
PmSocket(server) | Synchronous constructor; requires an aiohttp_ws.Server. | Stores server and one message/disconnect callback. It does not open a listener or create a socket yet. |
adapter.route(path, method="*", ...) | Returns a decorator for a sync/async function receiving request; the decorated name becomes a route wrapper. | Registers on adapter.server.routes. Return None from the function to upgrade. Use one uppercase method string, no response caching, and pass_cookies=False for WebSocket routes. |
adapter.handler(func) / adapter(func) | Both register func(ws, msg) and return the adapter. | One handler per adapter; later registration replaces it for every connection. Both sync and async callbacks work, but async is needed to await socket operations. |
adapter.on_disconnect(func) | Registers async func(ws) and returns the original function. | Runs after ordinary receive iteration. It is not a guaranteed finally hook, so handler exceptions require application handling. |
adapter.handle_msg(ws, msg) | Async; returns whatever the application callback returns. | Processes reserved text controls, then dispatches to the application callback. The route loop ignores the callback's return value; send explicitly using ws. |
ws: aiohttp.web.WebSocketResponse | Created internally after the route accepts the request. | Each connection has a distinct object. Await send_str(text), send_json(value), send_bytes(bytes), or close(). closed and close_code describe transport state. The framework does not add a connection registry. |
msg: aiohttp.WSMessage | Supplied to handler(ws, msg). | Read type before using data. This is a native aiohttp message, not the JSON-envelope aiohttp_ws.WSMessage used by WSEndpoint. A returned dict is not automatically sent. |
Connection-specific state can be held in an application dictionary keyed by ws. The route runs before ws exists, so record the socket when its first application message arrives. Keep arbitrary business data out of reserved control strings.
This server responds to the browser request example in the JavaScript guide. JSON encoding and the request_id field belong to this application protocol, not PmSocket itself.
import json
import aiohttp
from aiohttp_ws import Server, PmSocket
server = Server(host="127.0.0.1", port=8080)
adapter = PmSocket(server)
@adapter.route("/state/ws", method="GET")
async def accept(request):
return None
@adapter.handler
async def handle(ws, msg):
if msg.type != aiohttp.WSMsgType.TEXT or msg.data.startswith("::"):
return
try:
payload = json.loads(msg.data)
except json.JSONDecodeError:
await ws.send_json({"error": "invalid JSON"})
return
if not isinstance(payload, dict):
return
if payload.get("operation") == "get_state":
await ws.send_json({"request_id": payload.get("request_id"),
"type": "state", "data": {"ready": True}})
server.run()Use this alternative handler with an adapter already created above. Clients join by sending the text join. This registry tracks joined clients, not sockets that have connected without sending that message.
import asyncio
import aiohttp
joined = set()
@adapter.handler
async def chat(ws, msg):
if msg.type != aiohttp.WSMsgType.TEXT or msg.data.startswith("::"):
return
if msg.data == "join":
joined.add(ws)
await ws.send_str("joined")
return
if ws not in joined:
return
peers = [peer for peer in joined if not peer.closed]
results = await asyncio.gather(
*(peer.send_str(msg.data) for peer in peers),
return_exceptions=True,
)
for peer, result in zip(peers, results):
if isinstance(result, Exception):
joined.discard(peer)
@adapter.on_disconnect
async def cleanup(ws):
joined.discard(ws)Another alternative handler accepts binary frames up to an application limit. WebSocketResponse already has its own transport limits; this rule additionally defines what this application accepts. A socket callback's return value alone does not send a response.
import aiohttp
@adapter.handler
async def binary_ingest(ws, msg):
if msg.type == aiohttp.WSMsgType.TEXT:
if not msg.data.startswith("::"):
await ws.send_json({"error": "send a binary frame"})
return
if msg.type != aiohttp.WSMsgType.BINARY:
return
if len(msg.data) > 65536:
await ws.send_json({"error": "fragment exceeds 64 KiB"})
await ws.close()
return
await ws.send_json({"accepted_bytes": len(msg.data)})ValueError: Duplicate route.TypeError: 'NoneType' object is not callablefinally block. An exception from the message handler can bypass it; handle application errors inside the handler if cleanup is required.async def.::connect:: immediately after preparation. Initialize connection state before returning None, or lazily in the message handler.::ping:: and ::disconnect:: are still forwarded to the application handler. Filter the reserved protocol strings.compress=False and heartbeat=5; change the implementation or use a direct aiohttp WebSocket route.