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()