Skip to content

fix: raise MessageParseError for non-dict message fields in parse_message#1090

Open
Otis0408 wants to merge 1 commit into
anthropics:mainfrom
Otis0408:fix/parse-message-typeerror-non-dict
Open

fix: raise MessageParseError for non-dict message fields in parse_message#1090
Otis0408 wants to merge 1 commit into
anthropics:mainfrom
Otis0408:fix/parse-message-typeerror-non-dict

Conversation

@Otis0408

@Otis0408 Otis0408 commented Jul 8, 2026

Copy link
Copy Markdown

Summary

parse_message() documents Raises: MessageParseError: If parsing fails, and the maintainers already enforce this one level down: test_parse_assistant_string_content_raises and test_non_dict_content_block_raises_documented_error assert that a malformed content value surfaces as MessageParseError, "never a raw TypeError".

That guard stops at content. When the enclosing object is present but is not a dict, the nested subscript raises a raw TypeError that escapes the except KeyError-only handlers and reaches the caller, violating the documented contract:

input branch raised on main
{"type": "user", "message": "hi"} user TypeError: string indices must be integers
{"type": "assistant", "message": None} assistant TypeError: 'NoneType' object is not subscriptable
{"type": "rate_limit_event", "rate_limit_info": "x", ...} rate_limit_event TypeError (info["status"])
{"type": "result", ..., "deferred_tool_use": "oops"} result TypeError (deferred["id"])

Because neither public call site wraps parse_message (ClaudeSDKClient.receive_messages() / receive_response() and the query() path), these reach SDK consumers as a raw TypeError, so callers that except MessageParseError (or the exported base ClaudeSDKError) miss them.

Fix

Add isinstance guards mirroring the existing isinstance(raw_content, list) check, so a non-dict message / rate_limit_info / deferred_tool_use raises a descriptive MessageParseError instead of a raw TypeError.

  • No happy-path behavior change.
  • The except KeyError handlers and their existing messages (asserted by tests) are untouched, so missing-field cases still report "Missing required field ...".
  • Guards raise inside the existing try blocks; since MessageParseError is not a KeyError it propagates as documented (same as the current content guard).

Tests

Adds regression tests covering all four branches, parametrized over str / list / None / int message values. Verified they fail on main with the raw TypeError and pass with the fix.

1066 passed, 5 skipped
ruff check: All checks passed!
mypy src: Success: no issues found in 24 source files

…sage

parse_message() documents `Raises: MessageParseError`, and existing tests
(test_parse_assistant_string_content_raises,
test_non_dict_content_block_raises_documented_error) already enforce that a
malformed `content` value surfaces as MessageParseError, "never a raw
TypeError". That guard only covered one nesting level, though: when the
enclosing `message` (user/assistant), `rate_limit_info`, or
`deferred_tool_use` object is present but is not a dict, the nested subscript
(e.g. data["message"]["content"], info["status"], deferred["id"]) raised a
raw TypeError that escaped the `except KeyError`-only handlers and reached the
caller, bypassing the documented contract.

Add isinstance guards mirroring the existing `isinstance(raw_content, list)`
check so a non-dict `message` / `rate_limit_info` / `deferred_tool_use`
raises a descriptive MessageParseError instead of a raw TypeError. Adds
regression tests covering all four parse branches.
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.

1 participant