From b8e29e9b1e12356fc500bb0a0a5a9d70bfe0c707 Mon Sep 17 00:00:00 2001 From: ameer Date: Sat, 2 May 2026 17:31:25 +0800 Subject: [PATCH] fix(room): widen WS handler exception scope to JSONDecodeError + RuntimeError A single malformed JSON message (or a "WebSocket is not connected" race on disconnect) was killing the per-client handler with an uncaught exception in the ASGI app. The surrounding try/except only caught WebSocketDisconnect, so the server would log a stack trace and the client would silently drop. Wrap receive_json() to catch JSONDecodeError, send a structured {"type":"error","code":"bad_message"} ack, and continue. Widen the outer except to (WebSocketDisconnect, RuntimeError) so disconnect races on send/receive after close exit the handler cleanly instead of bubbling up the ASGI stack. Both student_ws and instructor_ws hardened in parallel. 31/31 pytest suite still passes; this fixes the recurring fuzz-scenario warn and the cycle-187-style cascade observed in the stress loop. --- app/room.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/room.py b/app/room.py index b20c1b0..09afe24 100644 --- a/app/room.py +++ b/app/room.py @@ -84,7 +84,14 @@ class RoomManager: try: await self.send_student_snapshot(websocket, sid, identity) while True: - data = await websocket.receive_json() + try: + data = await websocket.receive_json() + except json.JSONDecodeError: + try: + await websocket.send_json({"type": "error", "code": "bad_message", "message": "Invalid JSON"}) + except (WebSocketDisconnect, RuntimeError): + break + continue msg_type = data.get("type") if msg_type == "ping": await websocket.send_json({"type": "pong"}) @@ -93,7 +100,7 @@ class RoomManager: await websocket.send_json(ack) else: await websocket.send_json({"type": "error", "code": "bad_message", "message": "Unknown message type"}) - except WebSocketDisconnect: + except (WebSocketDisconnect, RuntimeError): pass finally: self.student_clients[sid].pop(websocket, None) @@ -104,7 +111,14 @@ class RoomManager: try: await self.send_instructor_snapshot(websocket, sid) while True: - data = await websocket.receive_json() + try: + data = await websocket.receive_json() + except json.JSONDecodeError: + try: + await websocket.send_json({"type": "error", "code": "bad_message", "message": "Invalid JSON"}) + except (WebSocketDisconnect, RuntimeError): + break + continue msg_type = data.get("type") if msg_type == "ping": await websocket.send_json({"type": "pong"}) @@ -118,7 +132,7 @@ class RoomManager: await self.end_session(sid) else: await websocket.send_json({"type": "error", "code": "bad_message", "message": "Unknown message type"}) - except WebSocketDisconnect: + except (WebSocketDisconnect, RuntimeError): pass finally: self.instructor_clients[sid].discard(websocket)