{"id":16450908,"url":"https://github.com/dontpanico/fastapi-distributed-websocket","last_synced_at":"2025-03-16T17:36:52.462Z","repository":{"id":37425249,"uuid":"444730154","full_name":"DontPanicO/fastapi-distributed-websocket","owner":"DontPanicO","description":"A library to implement websocket for distibuted system based on FastAPI.","archived":false,"fork":false,"pushed_at":"2023-08-30T16:23:55.000Z","size":178,"stargazers_count":57,"open_issues_count":13,"forks_count":10,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-16T04:31:42.723Z","etag":null,"topics":["async","asyncio","backend","fastapi","python","python3","python310","python311","starlette","websocket","websocket-server","websockets"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DontPanicO.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-05T08:50:47.000Z","updated_at":"2025-01-27T12:53:56.000Z","dependencies_parsed_at":"2024-11-15T05:30:42.735Z","dependency_job_id":null,"html_url":"https://github.com/DontPanicO/fastapi-distributed-websocket","commit_stats":{"total_commits":251,"total_committers":6,"mean_commits":"41.833333333333336","dds":"0.43027888446215135","last_synced_commit":"6a811a245e48f3168b136571766742dfd0d7d40c"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DontPanicO%2Ffastapi-distributed-websocket","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DontPanicO%2Ffastapi-distributed-websocket/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DontPanicO%2Ffastapi-distributed-websocket/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DontPanicO%2Ffastapi-distributed-websocket/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DontPanicO","download_url":"https://codeload.github.com/DontPanicO/fastapi-distributed-websocket/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243909078,"owners_count":20367524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asyncio","backend","fastapi","python","python3","python310","python311","starlette","websocket","websocket-server","websockets"],"created_at":"2024-10-11T10:06:18.358Z","updated_at":"2025-03-16T17:36:52.098Z","avatar_url":"https://github.com/DontPanicO.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/) \n[![License: MIT](https://img.shields.io/badge/License-MIT-success.svg)](https://mit-license.org/) \n[![pypi 0.2.0](https://img.shields.io/badge/pypi-0.2.0-ff69b4.svg)](https://pypi.org/project/fastapi-distributed-websocket/)\n\n# FastAPI Distributed Websocket\n\nA library to implement websocket for distibuted systems based on FastAPI.\n\n**N.B.: This library is still at an early stage, use it in production at your own risk.**\n\n\n## What it does\n\nThe main features of this libarary are:\n\n* Easly implementing broadcasting, pub/sub, chat rooms, etc...\n* Proxy websocket connections to other servers (e.g. from an api gateway)\n* Authentication\n* Clean exception handling\n* An in memory broker for fast development\n\n\n## Problems of scaling websocket among multiple servers in production\n\nWebsocket is a relatively new protocol for real time communication over HTTP.\nIt establishes a durable, stateful, full-duplex connection between clients and the server.\nIt can be used to implement chats, real time notifications, broadcasting and\npub/sub models.\n\n### Connections from clients\n\nHTTP request/response mechanism fits very well to scale among multiple server\ninstances in production. Any time a client makes a request, it can connect\nto any server instance and it's going to receive the same response. After\nthe response has been returned to the client, it went disconnected and it can\nmake another request without the need to hit the same instace as before.\nThis thanks to the stateless nature of HTTP.\n\nHowever, Websocket establishes a stateful connection between the client and the\nserver and, if some error occurs and the connection went lost, we have to\nensure that clients are going to hit the same server instance they were connected\nbefore, since that instance was managing the connection state.\n\n**Stateful means that there is a state that can be manipulated. In particular,\na stateful connection is a connection that heavily relies on its state in\norder to work**\n\n### Broadcasting and group messages\n\nAnother problem of scaling Websocket occurs when we need to send messages to\nmultiple connected clients (i.e. broadcasting a message or sending a message to\nall clients subscribed to a specific topic).\n\nImagine that we have a chat server, and that when an user send a message in a\nspecific chat room, we broadcast it to all users subscribed to that room.\nIf we have a single server instance, all connection are managed by this instance\nso we can safely trust that the message will be delivered to all recipents.\nOn the other hand, with multiple server instances, users subscribing to a chat\nroom will probably connect to different instances. This way, if an user send a\nmessage to the chat room *'xyz'* at the server *A*, users subscribed to the same\nchat room at the server *B* are not receiving it.\n\n### Documenting Websocket interfaces\n\nAnother common problem with Websocket, that's not even related to scaling, is\nabout documentation. Due to the event driven nature of the Websocket protocol\nit does not fit well to be documented with [openapi](https://swagger.io/specification/).\nHowever a new specification for asynchronous, event driven interfaces has been\ndefined recently. The spec name is [asyncapi](https://www.asyncapi.com/) and I'm\ncurrently studying it. I don't know if this has to be implemented here or it's\nbetter having a separate library for that, however this is surely something\nwe have to look at.\n\n### Other problems\n\nWhen I came first to think about this library, I started making a lot of research\nof common problems related to Websocket on stackoverflow, reddit, github issues and\nso on. I found some interesting resource that are however related to the implementation\nitself. I picked up best solutions and elaborated my owns converging all of that in\nthis library.\n\n## Examples\n\n### Installation\n\n`$ pip install fastapi-distributed-websocket`\n\n### Basic usage\n\nThis is a basic example using an in memory broker with a single server instance.\n\n```python\nfrom fastapi import FastAPI, WebSocket, WebSocketDisconnect, status\nfrom distributed_websocket import Connection, WebSocketManager\n\napp = FastAPI()\nmanager = WebSocketManager('channel:1', broker_url='memory://')\n...\n\n\napp.on_event('startup')\nasync def startup() -\u003e None:\n    ...\n    await manager.startup()\n\n\napp.on_event('shutdown')\nasync def shutdown() -\u003e None:\n    ...\n    await manager.shutdown()\n\n\n@app.websocket('/ws/{conn_id}')\nasync def websocket_endpoint(\n    ws: WebSocket,\n    conn_id: str,\n    *,\n    topic: Optional[Any] = None,\n) -\u003e None:\n    connection: Connection = await manager.new_connection(ws, conn_id)\n    try:\n        while True:\n            msg = await connection.receive_json()\n            await manager.broadcast(msg)\n    except WebSocketDisconnect:\n        await manager.remove_connection(connection)\n```\n\nThe `manger.new_connection` method create a new Connection object and add it to\nthe `manager.active_connections` list. Note that after a `WebSocketDisconnect`\nis raised, we call `remove_connection`: this method only remove the connection\nobject from the `manager.active_connections` list, without calling `connection.close`, since\nthe connection is already closed.\nIf you need to close a connection at any other time, you can use `manager.close_connection`.\nIf you use `connection.iter_json`, it already handles the `WebSocketDisconnect` exception, so\nyou can simply call `manager.remove_connection` just after the loop (see next code block).\n\nNote that here we are using `manager.broadcast` to send the message to all connections managed\nby the WebSocketManager instance. However, this method only work if we have a single server\ninstance. If we have multiple server instances, we have to use `manager.receive`, to properly\nsend the message to the broker.\n\n```python\n@app.websocket('/ws/{conn_id}')\nasync def websocket_endpoint(\n    ws: WebSocket,\n    conn_id: str,\n    *,\n    topic: Optional[Any] = None,\n) -\u003e None:\n    connection: Connection = await manager.new_connection(ws, conn_id)\n    # This is the preferred way of handling WebSocketDisconnect\n    async for msg in connection.iter_json():\n        await manager.receive(connection, msg)\n    await manager.remove_connection(connection)\n```\n\n### Proxy from an API gateway\n\nLet's say we are developing a chat service and that all our services are behind\nan API gateway. If we want to keep our websocket service behind it too, then\nfastapi-distributed-websocket provides us with `WebSocketProxy`.\n\n```python\nfrom distributed_websocket import WebSocketProxy\n# skipped other imports for brevity\n\napp = FastAPI()\n\n\nWS_TARGET_ENDPOINT = 'ws://websocket_service:8000/wshandler'\n\n@app.websocket('/ws')\nasync def websocket_proxy(websocket: WebSocket):\n    await websocket.accept()\n    ws_proxy = WebSocketProxy(websocket, WS_TARGET_ENDPOINT)\n    await ws_proxy()\n```\n\nThis will forward all messages from the client to the target endpoint and\nall messages from the target endpoint to the client.\n\nNow let's assume that our websocket service code is the code of our previous\nexample. Our API Gateway code will be:\n\n```python\nfrom distributed_websocket import WebSocketProxy\n# skipped other imports for brevity\n\napp = FastAPI()\n\n\nWS_TARGET_ENDPOINT = 'ws://websocket_service:8000/ws/{}'\n\n@app.websocket('/ws/{conn_id}')\nasync def websocket_endpoint(\n    ws: WebSocket,\n    conn_id: str,\n) -\u003e None:\n    await websocket.accept()\n    ws_proxy = WebSocketProxy(websocket, WS_TARGET_ENDPOINT.format(conn_id))\n    await ws_proxy()\n```\n\n## API Reference\n\n### Connection\n\nConnection objects wrap the websocket connection and provide a simple interface\nto send and receive messages. They have a `topics` attribute to store subscriptions\npatterns and implement pub/sub models.\n\n* **`async`**` accept(self) -\u003e None` \\\n  Accept the connection.\n* **`async`**` close(self, code: int = 1000) -\u003e None` \\\n  Close the connection with the specified status.\n* **`async`**` receive_json(self) -\u003e Any` \\\n  Receive a JSON message.\n* **`async`**` send_json(self, data: Any) -\u003e None` \\\n  Send a JSON message over the connection.\n* **`async`**` iter_json(self) -\u003e AsyncIterator[Any]` \\\n  Iterate over the messages received over the connection.\n\n\n### Messages\n\nMessage objects store the message type, the topic and the data. They provides\nan easy serialization/deserialization mechanism.\nRemeber that messages returned by `connection.iter_json` are already deserialized\ninto `dict` objects, so here we call *deserialization* the process of converting\na `dict` object into a `Message` object.\n\n* `type: str` \\\n  The message type.\n* `topic: str` \\\n  The message topic.\n* `conn_id: str | list[str]` \\\n  The connection id or list of connection ids that the message should be sent to.\n* `data: Any` \\\n  The message data.\n\n* **`classmethod`**`from_client_message(cls, *, data: Any) -\u003e Message` \\\n  Create a message from a client message.\n* `__serialize__(self) -\u003e dict` \\\n  Serialize the message into a `dict` object.\n\n\n### Subscriptions\n\nYou can bind topics to connection objects to implement pub/sub models, notification and so on.\nThe `topics` attribute is a set of strings that follows the pattern matching syntax of MQTT.\nThis library share connection objects state between server instances, so you may find\nreferences to terms like `channel`, `publish`, `subscribe` and `unsubscribe` referring to\nthe same concepts but applied to the underlying server/broker communication. \\\nThis may be confusing, but remember to keep separated the communication between the server\nand the clients, that you are developing and the communication between the server and the broker,\nthat you usually don't deal with.\n\n* `subscribe(connection: Connection, message: Message) -\u003e None` \\\n  Subscribe a connection to `message.topic`.\n* `unsubscribe(connection: Connection, message: Message) -\u003e None` \\\n  Unsubscribe a connection from `message.topic`.\n* `hanlde_subscription_message(connection: Connection, message: Message) -\u003e None` \\\n  Calls `subscribe` or `unsubscribe` depending on the message type.\n* `matches(topic: str, patterns: set[str]) -\u003e bool` \\\n  Check if `topic` matches any of the patterns in `patterns`.\n\n\n### Authentication\n\nAuthentication is provided with the `WebSocketOAuth2PasswordBearer` class.\nIt inherits from *FastAPI* `OAuth2PasswordBearer` and overrides `__call__` method to accept\na `WebSocket` object.\n\n* **`async`**` __call__(self, websocket: WebSocket) -\u003e str | None` \\\n  Authenticate the websocket connection and return the *Authorization* header value. \\\n  If the authentication fails, return `None` if the objects has been initialized with `auto_error=False` \\\n  or close the connection with the `WS_1008_POLICY_VIOLATION` code.\n\n\n### Exceptions and Exception Handling\n\n`fastapi-distributed-websocket` provides exception handling via decorators. You can use the\napposite decorators passing an exception class and a handler callable. Exception handlers\nshould accept only the exception object as argument.\\\n**Why this is useful?**\\\nBecause sometimes the same type of exception can be raised by different parts of the application,\nthis way you can decorate the higer level function in the call stack to handle the exception at\nany level.\\\nA base `WebSocketException` class is provided to bind connection objects to the exception, so\nyour handler function can easily access it.\nIf you need to access connection objects from the exception handler, your custom exceptions\nshould inherit from `WebSocketException`, no matter if they are really network related or not.\n\n* `WebSocketException(self, message: str, *, connection: Connection) -\u003e None`\n* `InvalidSubscription(self, message: str, *, connection: Connection) -\u003e None` \\\n  Raised when a subscription pattern use an invalid syntax. Inherits from `WebSocketException`.\n* `InvalidSubscriptionMessage(self, message: str, *, connection: Connection) -\u003e None` \\\n  Like `InvalidSubscription` it could be raised for bad syntax, but it could also be raised \\\n  when the message type is not *subscribe* or *unsubscribe*. Inherits from `WebSocketException`.\n\n* `handle(exc: BaseException, handler: Callable[..., Any]) -\u003e Callable[..., Any]` \\\n  Decorator to handle exceptions. If you decorate a function with this decorator, at any time \\\n  an exception of type `exc` is raised or propagated to the function, it will be handled by `handler`. \\\n  Use this decorator only if both your handler and the function are not async.\n* **`async`**` ahandle(\n    exc: BaseException, handler: Callable[..., Coroutine[Any, Any, Any]]\n) -\u003e Callable[..., Any]` \\\n  Decorator to handle exceptions, same ad `handle`, but the handler is a coroutine function. \\\n  Use this if your handler is a coroutine function, while the decorated function could be \\\n  either a sync or an async function.\n\n\n### Broker Interfaces\n\nConnections' state is shared between server instances using a pub/sub broker. By default,\nthe broker is a `reids.asyncio.Redis` instance (ex `aioredis.Redis`), but you can use any\nother implementation. `fastapi-distributed-websocket` provides an `InMemoryBroker` class\nfor development purposes.\nYou can inherit from `BrokerInterface` and override the methods to implement your own broker.\n\n* **`async`**` connect(self) -\u003e Coroutine[Any, Any, None]` \\\n  Connect to the broker.\n* **`async`**` disconnect(self) -\u003e Coroutine[Any, Any, None]` \\\n  Disconnect from the broker.\n* **`async`**` subscribe(self, channel: str) -\u003e Coroutine[Any, Any, None]` \\\n  Subscribe to a channel.\n* **`async`**` unsubscribe(self, channel: str) -\u003e Coroutine[Any, Any, None]` \\\n  Unsubscribe from a channel.\n* **`async`**` publish(self, channel: str, message: Any) -\u003e Coroutine[Any, Any, None]` \\\n  Publish a message to a channel.\n* **`async`**` get_message(self, **kwargs) -\u003e Coroutine[Any, Any, Message | None]` \\\n  Get a message from the broker.\n\n### WebSocketManager\n\nThe `WebSocketManager` class is where the main logic of the library is implemented. \\\nIt keeps track of the connection objects and starts the broker connection.\nIt spawn a main task, a listener that wait (non-blocking) for messages from the broker,\nand send them to the connection objects (broadcasting or checking for subscriptions)\nspawning a new task for each send. \\\nThe broker initialisation is done in the constructor while calls to `broker.connect` and\n`broker.disconnect` are handled in the `startup` and `shutdown` methods.\n\n* **`async`**` new_connection(\n        self, websocket: WebSocket, conn_id: str, topic: str | None = None\n    ) -\u003e Coroutine[Any, Any, Connection]` \\\n  Create a new connection object, add it to `self.active_connections` and return it.\n* **`async`**` close_connection(\n        self, connection: Connection, code: int = status.WS_1000_NORMAL_CLOSURE\n    ) -\u003e Coroutine[Any, Any, None]` \\\n  Close a connection object and remove it from `self.active_connections`.\n* ` remove_connection(self, connection: Connection) -\u003e None` \\\n  Remove a connection object from `self.active_connections`.\n* `set_conn_id(self, connection: Connection, conn_id: str) -\u003e None` \\\n  Set the connection id and notify the client.\n* `send(self, message: Message) -\u003e None` \\\n  Send a message to all the connection objects subscribed to `message.topic`. \\\n  It spawns a new task wrapping the coroutine resulting from `self._send`.\n* `broadcast(self, message: Message) -\u003e None` \\\n  Send a message to all the connection objects. \\\n  It spawns a new task wrapping the coroutine resulting from `self._broadcast`.\n* `send_by_conn_id(self, message: Message) -\u003e None` \\\n  Send a message to all the connection objects with `id` equals to `message.conn_id`. \\\n  It spawns a new task wrapping the coroutine resulting from `self._send_by_conn_id` \\\n  if `conn_id` is a string or from `_send_multi_by_conn_id` if it is a list.\n* `send_msg(self, message: Message) -\u003e None` \\\n  Based on the message type, it calls `send`, `send_by_conn_id` or `broadcast`.\n* **`async`**` receive(\n        self, connection: Connection, message: Any\n    ) -\u003e Coroutine[Any, Any, None]` \\\n  Receive a message from a connection object. It passes the message down to \\\n  a private method that handle eventual subscriptions and then publish the message \\\n  to the broker.\n* **`async`**` startup(self) -\u003e Coroutine[Any, Any, None]` \\\n  Start the broker connection and the listener task.\n* **`async`**` shutdown(self) -\u003e Coroutine[Any, Any, None]` \\\n  Close the broker connection and the listener task. \\\n  It also takes care to cancel all the tasks spawned by `send_msg` and \\\n  close all the connection objects before.\n\n\n### WebSocketProxy\n\nThe `WebSocketProxy` class initialise callable objects that can be\nused to start proxyng websocket messages from client to a server and viceversa.\nIt's initialised with a two parameters:\n\n* **client**: a `WebSocket` object\n* **server_endpoint**: a `str` containing the endpoint of the server\n\nNotice that the target server could be a remote server or the same server that starts the proxy.\n\n* **`async`**` __call__(self) -\u003e Coroutine[Any, Any, None]` \\\n  Start a websocket connection to **server_endpoint** and spawn two tasks: \\\n  one that forwards the messages from the client to the target and the other that \\\n  forwards the messages from the target to the client.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdontpanico%2Ffastapi-distributed-websocket","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdontpanico%2Ffastapi-distributed-websocket","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdontpanico%2Ffastapi-distributed-websocket/lists"}