From d4acaefee9b2acdc6ccbd6e2a5d137098c3e360d Mon Sep 17 00:00:00 2001 From: Alexander Whitestone <8633216+AlexanderWhitestone@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:09:03 -0500 Subject: [PATCH] fix: resolve WebSocket crashes from websockets 16.0 incompatibility (#97) The /ws redirect handler crashed with AttributeError because websockets 16.0 removed the legacy transfer_data_task attribute. The /swarm/live endpoint could also error on early client disconnects during accept. Co-authored-by: Alexander Payne Co-authored-by: Claude Opus 4.6 --- src/dashboard/app.py | 10 +++++++++- src/dashboard/routes/swarm.py | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/dashboard/app.py b/src/dashboard/app.py index ccbac18..b007ef6 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -545,9 +545,17 @@ async def ws_redirect(websocket: WebSocket): an endpoint. Stale browser tabs retry forever, spamming 403 errors. Accept the connection and immediately close with a policy-violation code so the client stops retrying. + + websockets 16.0 dropped the legacy ``transfer_data_task`` attribute, + so calling ``websocket.close()`` after accept triggers an + AttributeError. Use the raw ASGI send instead. """ await websocket.accept() - await websocket.close(code=1008, reason="Use /swarm/live instead") + try: + await websocket.close(code=1008, reason="Use /swarm/live instead") + except AttributeError: + # websockets >= 16.0 — close via raw ASGI message + await websocket.send({"type": "websocket.close", "code": 1008}) @app.get("/", response_class=HTMLResponse) diff --git a/src/dashboard/routes/swarm.py b/src/dashboard/routes/swarm.py index 859dbff..1908f31 100644 --- a/src/dashboard/routes/swarm.py +++ b/src/dashboard/routes/swarm.py @@ -409,7 +409,11 @@ def submit_bid(bid: BidRequest): @router.websocket("/live") async def swarm_live(websocket: WebSocket): """WebSocket endpoint for live swarm event streaming.""" - await ws_manager.connect(websocket) + try: + await ws_manager.connect(websocket) + except Exception as exc: + logger.warning("WebSocket accept failed: %s", exc) + return try: while True: data = await websocket.receive_text()