Skip to content

Commit 2f6a364

Browse files
committed
Merge #806: guard WS on_message against non-dict JSON
2 parents e4de2ff + 228fdbf commit 2f6a364

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

buckaroo/server/websocket_handler.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,27 @@ def on_message(self, message):
6161
self.write_message(json.dumps({"type": "error", "error_code": "invalid_json", "message": "Invalid JSON"}))
6262
return
6363

64+
# Reject non-object JSON (null, arrays, scalars) explicitly.
65+
# Without this guard, ``msg.get(...)`` below raises ``AttributeError``
66+
# on ``None`` etc., Tornado swallows it, and the WS closes
67+
# permanently. See #805.
68+
if not isinstance(msg, dict):
69+
self.write_message(json.dumps({"type": "error",
70+
"error_code": "invalid_message_shape",
71+
"message": f"WS messages must be JSON objects; got {type(msg).__name__}"}))
72+
return
73+
6474
msg_type = msg.get("type")
6575
if msg_type == "infinite_request":
6676
self._handle_infinite_request(msg.get("payload_args", {}))
6777
elif msg_type == "buckaroo_state_change":
6878
self._handle_buckaroo_state_change(msg.get("new_state") or {})
79+
else:
80+
# Pre-#805 unknown / missing ``type`` was silently dropped —
81+
# clients had no way to debug. Tell them what they sent.
82+
self.write_message(json.dumps({"type": "error",
83+
"error_code": "unknown_message_type",
84+
"message": f"Unknown WS message type: {msg_type!r}"}))
6985

7086
def _handle_buckaroo_state_change(self, new_state):
7187
sessions = self.application.settings["sessions"]

tests/unit/server/test_load_expr.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,54 @@ async def test_lazy_state_change_returns_explicit_error(self):
274274
finally:
275275
os.unlink(csv_path)
276276

277+
@tornado.testing.gen_test
278+
async def test_ws_message_robustness(self):
279+
"""Regression for #805: ``on_message`` did ``msg.get(...)`` on
280+
whatever ``json.loads`` returned, which is unsafe when the JSON
281+
is not an object (``null``, bare arrays, scalars). ``null`` in
282+
particular killed the WS — ``None.get`` raises ``AttributeError``,
283+
Tornado swallows it, the stream closes.
284+
285+
Adjacent: unknown message types (``{"type": 42}``, missing
286+
``type``, empty ``{}``) were silently dropped. Clients couldn't
287+
debug because no response came.
288+
289+
This test sends each malformed shape and asserts the server
290+
returns a structured error frame, NOT a silent drop or a
291+
crashed WS.
292+
"""
293+
import asyncio
294+
await _post(self.get_http_port(), "/load",
295+
{"session": "ws-guard",
296+
"path": "/tmp/restaurant-complaints-pandas.parquet",
297+
"mode": "buckaroo", "no_browser": True})
298+
ws = await tornado.websocket.websocket_connect(
299+
f"ws://localhost:{self.get_http_port()}/ws/ws-guard")
300+
await ws.read_message() # discard initial_state
301+
302+
# Each entry: (label, raw_ws_message). Server must respond
303+
# to each with a structured error frame within 2s.
304+
cases = [
305+
("bare_null", "null"),
306+
("bare_array", "[1,2,3]"),
307+
("bare_scalar", "42"),
308+
("empty_object", "{}"),
309+
("missing_type", json.dumps({"payload": "x"})),
310+
("type_as_int", json.dumps({"type": 42, "payload": "x"})),
311+
("unknown_type",
312+
json.dumps({"type": "buckaroo_invented_command"})),
313+
]
314+
for label, raw in cases:
315+
ws.write_message(raw)
316+
frame = await asyncio.wait_for(ws.read_message(), timeout=3.0)
317+
self.assertIsNotNone(frame, f"{label}: no response (silent drop)")
318+
d = json.loads(frame)
319+
self.assertEqual(d.get("type"), "error",
320+
f"{label}: expected error frame, got {d.get('type')!r}")
321+
self.assertIn("error_code", d,
322+
f"{label}: error frame missing error_code")
323+
ws.close()
324+
277325
@tornado.testing.gen_test
278326
async def test_session_reuse_xorq_then_pandas(self):
279327
"""A client that POSTs /load_expr and then POSTs /load with the

0 commit comments

Comments
 (0)