From ddb470cacb2c051aec541c09e31c2f923a6e3e45 Mon Sep 17 00:00:00 2001 From: Zeffut Date: Sat, 23 May 2026 21:30:14 +0000 Subject: [PATCH] fix(parser): surface TypeError as MessageParseError on malformed payloads parse_message documents that malformed messages raise MessageParseError, but each case-branch's try/except only catches KeyError. When a field is present but of the wrong type (e.g. rate_limit_event with rate_limit_info=None, or user/assistant with message=None), the subsequent indexing raises TypeError, which escapes the parser and crashes the read loop -- losing every subsequent message in the stream. Add a TypeError clause alongside the existing KeyError clause in the six case branches that index sub-fields. Existing "Missing required field..." wording is preserved for KeyError (backward-compatible with existing tests); the new clause emits "Malformed field..." through the same MessageParseError type and carries the original payload. Co-Authored-By: Claude --- .../_internal/message_parser.py | 24 ++++++++++++ tests/test_message_parser.py | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/claude_agent_sdk/_internal/message_parser.py b/src/claude_agent_sdk/_internal/message_parser.py index 574816c6..6b5b0577 100644 --- a/src/claude_agent_sdk/_internal/message_parser.py +++ b/src/claude_agent_sdk/_internal/message_parser.py @@ -122,6 +122,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in user message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in user message: {e}", data + ) from e case "assistant": try: @@ -184,6 +188,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in assistant message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in assistant message: {e}", data + ) from e case "system": try: @@ -242,6 +250,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in system message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in system message: {e}", data + ) from e case "result": try: @@ -275,6 +287,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in result message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in result message: {e}", data + ) from e case "stream_event": try: @@ -288,6 +304,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in stream_event message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in stream_event message: {e}", data + ) from e case "rate_limit_event": try: @@ -310,6 +330,10 @@ def parse_message(data: dict[str, Any]) -> Message | None: raise MessageParseError( f"Missing required field in rate_limit_event message: {e}", data ) from e + except TypeError as e: + raise MessageParseError( + f"Malformed field in rate_limit_event message: {e}", data + ) from e case _: # Forward-compatible: skip unrecognized message types so newer diff --git a/tests/test_message_parser.py b/tests/test_message_parser.py index 7ce2990c..49bba9b9 100644 --- a/tests/test_message_parser.py +++ b/tests/test_message_parser.py @@ -754,6 +754,43 @@ def test_message_parse_error_contains_data(self): parse_message(data) assert exc_info.value.data == data + def test_parse_rate_limit_event_with_non_dict_info(self): + """Malformed rate_limit_info (non-dict) raises MessageParseError, not TypeError. + + A buggy or older CLI may emit ``rate_limit_info`` as a non-dict (e.g. + ``None`` or a string). Such payloads must surface as + :class:`MessageParseError` like every other malformed-message case; + a raw ``TypeError`` would crash the parser loop and lose the rest + of the stream. + """ + for info_value in (None, "oops", 42): + data = { + "type": "rate_limit_event", + "rate_limit_info": info_value, + "uuid": "abc", + "session_id": "sess", + } + with pytest.raises(MessageParseError) as exc_info: + parse_message(data) + assert "rate_limit_event message" in str(exc_info.value) + assert exc_info.value.data == data + + def test_parse_user_message_with_non_dict_message(self): + """Malformed user message field (non-dict) raises MessageParseError.""" + data = {"type": "user", "message": None} + with pytest.raises(MessageParseError) as exc_info: + parse_message(data) + assert "user message" in str(exc_info.value) + assert exc_info.value.data == data + + def test_parse_assistant_message_with_non_dict_message(self): + """Malformed assistant message field (non-dict) raises MessageParseError.""" + data = {"type": "assistant", "message": None} + with pytest.raises(MessageParseError) as exc_info: + parse_message(data) + assert "assistant message" in str(exc_info.value) + assert exc_info.value.data == data + def test_parse_assistant_message_without_error(self): """Test that assistant message without error has error=None.""" data = {