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 = {