Skip to content

fix(parser): wrap non-dict message and rate_limit_info in MessageParseError - #1213

Open
Amey-Thakur wants to merge 1 commit into
anthropics:mainfrom
Amey-Thakur:fix/parser-non-dict-message-typeerror
Open

fix(parser): wrap non-dict message and rate_limit_info in MessageParseError#1213
Amey-Thakur wants to merge 1 commit into
anthropics:mainfrom
Amey-Thakur:fix/parser-non-dict-message-typeerror

Conversation

@Amey-Thakur

Copy link
Copy Markdown

Summary

parse_message wraps malformed frames in MessageParseError so callers can catch one documented exception and recover the offending frame from error.data. Two frame shapes slipped past that contract and raised a raw TypeError instead, which escapes parse_message and tears down the receive_messages() / query() async generator.

This continues the hardening from #1064 (non-dict assistant content and non-dict content blocks) and the rate_limit_event field guards (#599 / #601 / #689, which covered missing keys). The two sibling shapes below were still unguarded.

The gap

Both the user and assistant branches index data["message"]["content"] before checking that message is a dict, and the rate_limit_event branch reads info["status"] before checking that rate_limit_info is a dict.

from claude_agent_sdk._internal.message_parser import parse_message

parse_message({"type": "assistant", "message": "oops"})
# TypeError: string indices must be integers, not 'str'

parse_message({"type": "user", "message": 5})
# TypeError: 'int' object is not subscriptable

parse_message({"type": "rate_limit_event", "uuid": "u", "session_id": "s",
               "rate_limit_info": 5})
# TypeError: 'int' object is not subscriptable

Because the TypeError is not wrapped in MessageParseError, a consumer catching MessageParseError does not catch it, the raw frame is not attached for diagnosis, and the message stream is torn down. This is the same failure mode #1064 describes, and as that issue notes, Anthropic-compatible backends that emit slightly different frame shapes are the common trigger.

Fix

Add the isinstance(..., dict) guard, with the same MessageParseError wording already used for non-dict content blocks, at the two sites that were missing it:

  • user / assistant: validate data["message"] is a dict before indexing into it.
  • rate_limit_event: validate data["rate_limit_info"] is a dict before reading its keys.

Every frame that already parsed correctly is unchanged; only the three previously-crashing shapes now raise the documented MessageParseError instead of a raw TypeError.

Tests

tests/test_message_parser.py:

  • test_non_dict_message_raises_documented_error — parametrized over assistant / user and over a string, int, list, and None message.
  • test_non_dict_rate_limit_info_raises_documented_error — parametrized over the same non-dict values.

Both assert MessageParseError and the expected dict message. They fail with a raw TypeError without this change and pass with it. The full tests/test_message_parser.py suite is green (92 passed). ruff check, ruff format --check, and mypy on the changed files are clean.

…eError

parse_message wraps malformed frames in MessageParseError so callers can
catch one documented exception and recover the offending frame. anthropics#1064 added
that guard for non-dict assistant content and non-dict content blocks, and the
rate_limit_event guards (anthropics#599/anthropics#601/anthropics#689) covered missing keys.

Two sibling shapes were still unguarded: a message that is not a dict (the
assistant and user branches index data["message"]["content"] before any type
check) and a rate_limit_info that is not a dict (info["status"]). Both raised a
raw TypeError that escaped parse_message and tore down the
receive_messages()/query() generator, the same failure mode anthropics#1064 describes.
Anthropic-compatible backends that emit slightly different shapes are the cited
trigger.

Guard both with the isinstance check and MessageParseError wording already used
elsewhere in the module, and add regression tests for non-dict message
(assistant/user) and non-dict rate_limit_info.
Copilot AI lite review requested due to automatic review settings August 15, 2026 19:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants