Skip to content

Fix uncaught TypeError on string or non-dict message content#1058

Open
adventurelands wants to merge 1 commit into
anthropics:mainfrom
adventurelands:fix-assistant-string-content
Open

Fix uncaught TypeError on string or non-dict message content#1058
adventurelands wants to merge 1 commit into
anthropics:mainfrom
adventurelands:fix-assistant-string-content

Conversation

@adventurelands

@adventurelands adventurelands commented Jun 16, 2026

Copy link
Copy Markdown

Closes #1064.

Small fix: assistant message with string content raises an uncaught TypeError

Thanks for open-sourcing this.

Minor one. parse_message handles string content for user messages but not
for assistant messages, so an assistant message whose content is a plain
string gets iterated character by character and raises a TypeError instead of
the documented MessageParseError. A non-dict block in the content list does
the same in both branches.

from claude_agent_sdk._internal.message_parser import parse_message
parse_message({"type": "user",      "message": {"content": "hi"}})               # ok
parse_message({"type": "assistant", "message": {"model": "m", "content": "hi"}})  # TypeError

The patch makes assistant mirror the user branch (wrap a string as one
TextBlock) and raises MessageParseError on a non-dict block. Added a test
that fails on main and passes with the patch. Nothing else touched.

Just figured I would pass it along. Thanks again.

Davis Brief, davisbrief@gmail.com

parse_message handled string content for user messages but not assistant
messages, so assistant string content was iterated character by character
and raised TypeError instead of the documented MessageParseError. A non-dict
content block did the same in both branches. Mirror the user branch for
assistant and raise MessageParseError on non-dict blocks. Adds a regression
test that fails on main and passes with this change.

Co-Authored-By: Claude <noreply@anthropic.com>
@adventurelands

Copy link
Copy Markdown
Author

Filed #1064 with a minimal verified repro of the underlying crash (assistant content as a string and a non-dict block both raise an uncaught TypeError on latest main; the user branch already handles the string case). This PR closes it, with regression tests. Happy to rebase or reshape the string-to-TextBlock handling if you'd prefer a different approach. Thanks for taking a look.

@adventurelands

Copy link
Copy Markdown
Author

@ashwin-ant @qing-ant — small fix here closing #1064 (verified repro: an assistant message with content as a string or a non-dict block raises an uncaught TypeError on latest main; the user branch already handles the string case). +126/−1 with regression tests. Mind kicking off CI / a quick look? Happy to reshape the string→TextBlock handling if you'd prefer a different approach. Thanks!

@ashwin-ant ashwin-ant left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the PR! The non-dict block guard is a real fix and I'd like to land that part. A couple of things to sort out first though:

The else raw_content arm actually makes things worse. If content is something weird like None or a bare dict, the ternary now passes it straight through into AssistantMessage.content (which is typed list[ContentBlock]). On main that case blows up immediately with a TypeError inside the parser; with this patch it returns a silently-broken object and the crash moves downstream to whoever iterates msg.content. That's the opposite of what we want here.

The string-content case isn't actually a thing for assistant messages. The CLI emits BetaMessage for assistant turns, and BetaMessage.content is always an array — the string shorthand only exists on the input side (MessageParam). That's why AssistantMessage.content is list[ContentBlock] while UserMessage.content is str | list[...]. So rather than normalizing a string into [TextBlock(...)], non-list assistant content should just raise MessageParseError like the other guards you added.

That also means we don't need the second AssistantMessage(...) return — duplicating those 8 field mappings is a drift hazard. Something like this keeps it to one constructor:

raw_content = data["message"]["content"]
if not isinstance(raw_content, list):
    raise MessageParseError(
        f"Invalid assistant content (expected list, got {type(raw_content).__name__})",
        data,
    )
content_blocks: list[ContentBlock] = []
for block in raw_content:
    if not isinstance(block, dict):
        raise MessageParseError(...)
    ...

Small test cleanup while you're in there:

  • Drop the Place under "tests/"... line from the docstring
  • test_normal_block_lists_unaffected and test_user_string_content_still_parses duplicate existing tests in test_message_parser.py — can go
  • Swap the test_assistant_string_content_* tests for one that asserts MessageParseError on string content
  • If you don't mind, fold what's left into class TestMessageParser in tests/test_message_parser.py so it lives with the other parser tests

The isinstance(block, dict) guards and the parametrized error test are good as-is. 👍

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.

Assistant message with string or non-dict content raises uncaught TypeError (user branch handles it)

2 participants