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.
This commit is contained in:
ameer
2026-05-02 17:31:25 +08:00
parent 95a4dd2475
commit b8e29e9b1e

View File

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