diff --git a/src/acp/_transport.py b/src/acp/_transport.py index 9aa7ecc..1ded5e9 100644 --- a/src/acp/_transport.py +++ b/src/acp/_transport.py @@ -79,10 +79,13 @@ async def receive(self) -> dict[str, Any] | None: if not line: continue try: - message: dict[str, Any] = json.loads(line) + message = json.loads(line) except Exception: logging.exception("Error parsing JSON-RPC message") continue + if not isinstance(message, dict): + logging.warning("Ignoring non-object JSON-RPC message") + continue return message async def close(self) -> None: diff --git a/tests/test_connection_recovery.py b/tests/test_connection_recovery.py index 77059d3..67d58d3 100644 --- a/tests/test_connection_recovery.py +++ b/tests/test_connection_recovery.py @@ -121,3 +121,31 @@ async def test_receive_loop_does_not_swallow_unrelated_reader_error() -> None: with pytest.raises(ValueError, match="reader failed"): await conn._receive_loop() await conn.close() + + +@pytest.mark.asyncio +async def test_receive_loop_ignores_frames_that_are_not_json_objects() -> None: + """A line can be valid JSON and still not be a JSON-RPC message. + + Unparsable input is already skipped by ``NdjsonTransport.receive``. These frames parse + fine but are not objects: an array, a number, a string and ``null``. Without the object + guard they reach ``Connection._process_message`` -- which calls ``message.get(...)`` -- + or, for ``null``, are read as the transport's EOF signal. Either way the connection dies + and the valid frame queued behind them is never handled. + """ + conn, reader = _make_connection() + processed: list[str] = [] + + def tracking_process(message: dict[str, Any]) -> None: + processed.append(message["method"]) + + conn._process_message = tracking_process # type: ignore[method-assign] + non_objects = b"\n".join([b"[]", b"123", b'"x"', b"null", b"", b"not json at all"]) + survivor = {"jsonrpc": "2.0", "method": "survivor"} + reader.feed_data(non_objects + b"\n" + json.dumps(survivor).encode() + b"\n") + reader.feed_eof() + + await conn._receive_loop() + await conn.close() + + assert processed == ["survivor"]