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