Skip to content

Commit 585588f

Browse files
alicodingclaude
andcommitted
fix: Handle None message fields safely in get_message_content()
- Fixed AttributeError when msg['message'] is None - Added get_message_content() to public API for safe extraction - Added black box tests to prevent regression - Handles all None scenarios defensively Fixes bug reported by claude-explorer where msg.get('message', {}).get('content') would fail when message field exists but is None. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 275c1fd commit 585588f

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

claude_parser/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .session import SessionManager
1717
from .export import export_for_llamaindex
1818
from .filtering import filter_messages_by_type, filter_messages_by_tool, search_messages_by_content, exclude_tool_operations
19+
from .messages.utils import get_message_content, get_text
1920

2021
# Version info
2122
__version__ = "2.1.0"

claude_parser/messages/utils.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,27 @@ def is_hook_message(msg: Dict[str, Any]) -> bool:
8787

8888
def is_tool_operation(msg: Dict[str, Any]) -> bool:
8989
"""Check if message is a tool operation"""
90-
return bool(msg.get('tool_use_id') or msg.get('tool_result'))
90+
return bool(msg.get('tool_use_id') or msg.get('tool_result'))
91+
92+
93+
def get_message_content(msg: Dict[str, Any]) -> str:
94+
"""Safely extract content from message, handling None values.
95+
96+
@FRAMEWORK_FIRST: Handles the bug where msg['message'] = None
97+
"""
98+
# Direct content field
99+
if 'content' in msg:
100+
content = msg['content']
101+
if isinstance(content, str):
102+
return content
103+
elif isinstance(content, list):
104+
# Handle content blocks
105+
return get_text(msg)
106+
107+
# Nested message field - MUST check for None explicitly
108+
if 'message' in msg and msg['message'] is not None:
109+
nested = msg['message']
110+
if isinstance(nested, dict):
111+
return nested.get('content', '')
112+
113+
return ''

tests/test_message_none_bug.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Black Box Test: Message Content Extraction with None Values
4+
Tests that get_message_content() handles None message fields correctly
5+
"""
6+
7+
import pytest
8+
from claude_parser import get_message_content, get_text
9+
10+
11+
def test_message_none_bug_fixed():
12+
"""Test that msg['message'] = None doesn't cause AttributeError
13+
14+
This was the reported bug from claude-explorer where
15+
msg.get('message', {}).get('content', '') would fail
16+
"""
17+
# The bug case: message key exists but is None
18+
msg = {'message': None}
19+
20+
# Should not raise AttributeError
21+
content = get_message_content(msg)
22+
assert content == ''
23+
24+
# Also test with get_text
25+
text = get_text(msg)
26+
assert text == ''
27+
28+
29+
def test_message_content_safe_extraction():
30+
"""Test safe extraction with various None scenarios"""
31+
test_cases = [
32+
({'message': None}, ''), # Bug case
33+
({'content': 'direct'}, 'direct'), # Direct content
34+
({'message': {'content': 'nested'}}, 'nested'), # Nested
35+
({}, ''), # Empty
36+
({'content': None}, ''), # Content is None
37+
({'message': {}}, ''), # Empty message dict
38+
]
39+
40+
for msg, expected in test_cases:
41+
assert get_message_content(msg) == expected
42+
43+
44+
def test_content_block_extraction():
45+
"""Test extraction from content blocks"""
46+
msg = {'content': [{'type': 'text', 'text': 'Block text'}]}
47+
assert get_message_content(msg) == 'Block text'
48+
49+
# Empty blocks
50+
msg2 = {'content': []}
51+
assert get_message_content(msg2) == ''

0 commit comments

Comments
 (0)