Skip to content

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

Merged
ashwin-ant merged 2 commits into
anthropics:mainfrom
adventurelands:fix-assistant-string-content
Jun 26, 2026
Merged

Fix uncaught TypeError on string or non-dict message content#1058
ashwin-ant merged 2 commits into
anthropics:mainfrom
adventurelands:fix-assistant-string-content

Conversation

@adventurelands

@adventurelands adventurelands commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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
Contributor 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
Contributor 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. 👍

@adventurelands

adventurelands commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thoughtful response @ashwin-ant . A lot of helpful notes and instruction here. Makes sense on the raw_content arm, and appreciate the context for the string-content case. The suggested cleanups are reflected in the latest push.

@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!

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@7f74cdf). Learn more about missing BASE report.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1058   +/-   ##
=======================================
  Coverage        ?   89.37%           
=======================================
  Files           ?       23           
  Lines           ?     4009           
  Branches        ?        0           
=======================================
  Hits            ?     3583           
  Misses          ?      426           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ashwin-ant ashwin-ant merged commit d47b180 into anthropics:main Jun 26, 2026
7 checks passed
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)

3 participants