Skip to content

Commit ddb470c

Browse files
Zeffutclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 9970096 commit ddb470c

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

src/claude_agent_sdk/_internal/message_parser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
122122
raise MessageParseError(
123123
f"Missing required field in user message: {e}", data
124124
) from e
125+
except TypeError as e:
126+
raise MessageParseError(
127+
f"Malformed field in user message: {e}", data
128+
) from e
125129

126130
case "assistant":
127131
try:
@@ -184,6 +188,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
184188
raise MessageParseError(
185189
f"Missing required field in assistant message: {e}", data
186190
) from e
191+
except TypeError as e:
192+
raise MessageParseError(
193+
f"Malformed field in assistant message: {e}", data
194+
) from e
187195

188196
case "system":
189197
try:
@@ -242,6 +250,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
242250
raise MessageParseError(
243251
f"Missing required field in system message: {e}", data
244252
) from e
253+
except TypeError as e:
254+
raise MessageParseError(
255+
f"Malformed field in system message: {e}", data
256+
) from e
245257

246258
case "result":
247259
try:
@@ -275,6 +287,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
275287
raise MessageParseError(
276288
f"Missing required field in result message: {e}", data
277289
) from e
290+
except TypeError as e:
291+
raise MessageParseError(
292+
f"Malformed field in result message: {e}", data
293+
) from e
278294

279295
case "stream_event":
280296
try:
@@ -288,6 +304,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
288304
raise MessageParseError(
289305
f"Missing required field in stream_event message: {e}", data
290306
) from e
307+
except TypeError as e:
308+
raise MessageParseError(
309+
f"Malformed field in stream_event message: {e}", data
310+
) from e
291311

292312
case "rate_limit_event":
293313
try:
@@ -310,6 +330,10 @@ def parse_message(data: dict[str, Any]) -> Message | None:
310330
raise MessageParseError(
311331
f"Missing required field in rate_limit_event message: {e}", data
312332
) from e
333+
except TypeError as e:
334+
raise MessageParseError(
335+
f"Malformed field in rate_limit_event message: {e}", data
336+
) from e
313337

314338
case _:
315339
# Forward-compatible: skip unrecognized message types so newer

tests/test_message_parser.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,43 @@ def test_message_parse_error_contains_data(self):
754754
parse_message(data)
755755
assert exc_info.value.data == data
756756

757+
def test_parse_rate_limit_event_with_non_dict_info(self):
758+
"""Malformed rate_limit_info (non-dict) raises MessageParseError, not TypeError.
759+
760+
A buggy or older CLI may emit ``rate_limit_info`` as a non-dict (e.g.
761+
``None`` or a string). Such payloads must surface as
762+
:class:`MessageParseError` like every other malformed-message case;
763+
a raw ``TypeError`` would crash the parser loop and lose the rest
764+
of the stream.
765+
"""
766+
for info_value in (None, "oops", 42):
767+
data = {
768+
"type": "rate_limit_event",
769+
"rate_limit_info": info_value,
770+
"uuid": "abc",
771+
"session_id": "sess",
772+
}
773+
with pytest.raises(MessageParseError) as exc_info:
774+
parse_message(data)
775+
assert "rate_limit_event message" in str(exc_info.value)
776+
assert exc_info.value.data == data
777+
778+
def test_parse_user_message_with_non_dict_message(self):
779+
"""Malformed user message field (non-dict) raises MessageParseError."""
780+
data = {"type": "user", "message": None}
781+
with pytest.raises(MessageParseError) as exc_info:
782+
parse_message(data)
783+
assert "user message" in str(exc_info.value)
784+
assert exc_info.value.data == data
785+
786+
def test_parse_assistant_message_with_non_dict_message(self):
787+
"""Malformed assistant message field (non-dict) raises MessageParseError."""
788+
data = {"type": "assistant", "message": None}
789+
with pytest.raises(MessageParseError) as exc_info:
790+
parse_message(data)
791+
assert "assistant message" in str(exc_info.value)
792+
assert exc_info.value.data == data
793+
757794
def test_parse_assistant_message_without_error(self):
758795
"""Test that assistant message without error has error=None."""
759796
data = {

0 commit comments

Comments
 (0)