Skip to content

Commit bdb9ac7

Browse files
PerryLinkfrostming
andauthored
fix(transport): ignore JSON-RPC frames that are not objects (#153)
* fix(transport): ignore JSON-RPC frames that are not objects * Apply suggestion from @frostming --------- Co-authored-by: PerryLink <perrylink@users.noreply.github.com> Co-authored-by: Frost Ming <mianghong@gmail.com>
1 parent 2cd4c7e commit bdb9ac7

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

‎src/acp/_transport.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,13 @@ async def receive(self) -> dict[str, Any] | None:
7979
if not line:
8080
continue
8181
try:
82-
message: dict[str, Any] = json.loads(line)
82+
message = json.loads(line)
8383
except Exception:
8484
logging.exception("Error parsing JSON-RPC message")
8585
continue
86+
if not isinstance(message, dict):
87+
logging.warning("Ignoring non-object JSON-RPC message")
88+
continue
8689
return message
8790

8891
async def close(self) -> None:

‎tests/test_connection_recovery.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,31 @@ async def test_receive_loop_does_not_swallow_unrelated_reader_error() -> None:
121121
with pytest.raises(ValueError, match="reader failed"):
122122
await conn._receive_loop()
123123
await conn.close()
124+
125+
126+
@pytest.mark.asyncio
127+
async def test_receive_loop_ignores_frames_that_are_not_json_objects() -> None:
128+
"""A line can be valid JSON and still not be a JSON-RPC message.
129+
130+
Unparsable input is already skipped by ``NdjsonTransport.receive``. These frames parse
131+
fine but are not objects: an array, a number, a string and ``null``. Without the object
132+
guard they reach ``Connection._process_message`` -- which calls ``message.get(...)`` --
133+
or, for ``null``, are read as the transport's EOF signal. Either way the connection dies
134+
and the valid frame queued behind them is never handled.
135+
"""
136+
conn, reader = _make_connection()
137+
processed: list[str] = []
138+
139+
def tracking_process(message: dict[str, Any]) -> None:
140+
processed.append(message["method"])
141+
142+
conn._process_message = tracking_process # type: ignore[method-assign]
143+
non_objects = b"\n".join([b"[]", b"123", b'"x"', b"null", b"", b"not json at all"])
144+
survivor = {"jsonrpc": "2.0", "method": "survivor"}
145+
reader.feed_data(non_objects + b"\n" + json.dumps(survivor).encode() + b"\n")
146+
reader.feed_eof()
147+
148+
await conn._receive_loop()
149+
await conn.close()
150+
151+
assert processed == ["survivor"]

0 commit comments

Comments
 (0)