From 5bc542e92f5859676a7b8bb3bc17dd8a1bb4d41f Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 11 Jun 2026 03:29:16 +0000 Subject: [PATCH] fix(monitor): quiet send-after-close race on WS disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a monitor subscriber disconnects mid-frame (the client portal closes its stream on every tab switch via the Page Visibility guard), the loop could pull a queued payload during the 1s wait and then send_json into an already-closing socket -> "Unexpected ASGI message 'websocket.send' after ... websocket.close", logged as a WARNING on every disconnect. Re-check gone.done() after the queue wait and break before sending; treat the residual send-after-close as expected (debug, not warning). No behavior change — the connection was already closing as intended; this just stops the log spam. Co-Authored-By: Claude Opus 4.8 --- app/routers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/routers.py b/app/routers.py index be83f41..ae869bb 100644 --- a/app/routers.py +++ b/app/routers.py @@ -284,11 +284,21 @@ async def monitor_stream(websocket: WebSocket, unit_id: str): payload = await asyncio.wait_for(queue.get(), timeout=1.0) except asyncio.TimeoutError: continue # re-check gone.done() + if gone.done(): + break # client disconnected while we waited — don't send into a closing socket await websocket.send_json(payload) except WebSocketDisconnect: logger.info(f"Monitor subscriber disconnected for {unit_id}") except Exception as e: - logger.warning(f"Monitor stream error for {unit_id}: {e}") + # A frame that races the close (client vanished mid-send) surfaces as + # "Unexpected ASGI message 'websocket.send' after ... websocket.close". + # That's expected on disconnect (the portal closes the socket on every tab + # switch), not an error — log it quietly. + msg = str(e) + if "after sending" in msg or "websocket.close" in msg or "response already completed" in msg: + logger.debug(f"Monitor stream for {unit_id} closed mid-send (client gone)") + else: + logger.warning(f"Monitor stream error for {unit_id}: {e}") finally: gone.cancel() await monitor.unsubscribe(queue)