fix: validate empty user messages to prevent Anthropic API 400 errors (#3322)
When user messages have empty content (e.g., Discord @mention-only messages, unrecognized attachments), the Anthropic API rejects the request with 'user messages must have non-empty content'. Changes: - anthropic_adapter.py: Add empty content validation for user messages (string and list formats), matching the existing pattern for assistant and tool messages. Empty content gets '(empty message)' placeholder. - discord.py: Defense-in-depth check at gateway layer to catch empty messages before they enter session history. - Add 4 regression tests covering empty string, whitespace-only, empty list, and empty text block scenarios. Fixes #3143 Co-authored-by: Bartok9 <bartok9@users.noreply.github.com>
This commit is contained in:
@@ -706,14 +706,21 @@ def convert_messages_to_anthropic(
|
||||
result.append({"role": "user", "content": [tool_result]})
|
||||
continue
|
||||
|
||||
# Regular user message
|
||||
# Regular user message — validate non-empty content (Anthropic rejects empty)
|
||||
if isinstance(content, list):
|
||||
converted_blocks = _convert_content_to_anthropic(content)
|
||||
result.append({
|
||||
"role": "user",
|
||||
"content": converted_blocks or [{"type": "text", "text": ""}],
|
||||
})
|
||||
# Check if all text blocks are empty
|
||||
if not converted_blocks or all(
|
||||
b.get("text", "").strip() == ""
|
||||
for b in converted_blocks
|
||||
if isinstance(b, dict) and b.get("type") == "text"
|
||||
):
|
||||
converted_blocks = [{"type": "text", "text": "(empty message)"}]
|
||||
result.append({"role": "user", "content": converted_blocks})
|
||||
else:
|
||||
# Validate string content is non-empty
|
||||
if not content or (isinstance(content, str) and not content.strip()):
|
||||
content = "(empty message)"
|
||||
result.append({"role": "user", "content": content})
|
||||
|
||||
# Strip orphaned tool_use blocks (no matching tool_result follows)
|
||||
|
||||
@@ -2096,6 +2096,11 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
if pending_text_injection:
|
||||
event_text = f"{pending_text_injection}\n\n{event_text}" if event_text else pending_text_injection
|
||||
|
||||
# Defense-in-depth: prevent empty user messages from entering session
|
||||
# (can happen when user sends @mention-only with no other text)
|
||||
if not event_text or not event_text.strip():
|
||||
event_text = "(The user sent a message with no text content)"
|
||||
|
||||
event = MessageEvent(
|
||||
text=event_text,
|
||||
message_type=msg_type,
|
||||
|
||||
@@ -801,6 +801,48 @@ class TestConvertMessages:
|
||||
assert all(not (b.get("type") == "text" and b.get("text") == "") for b in assistant_blocks)
|
||||
assert any(b.get("type") == "tool_use" for b in assistant_blocks)
|
||||
|
||||
def test_empty_user_message_string_gets_placeholder(self):
|
||||
"""Empty user message strings should get '(empty message)' placeholder.
|
||||
|
||||
Anthropic rejects requests with empty user message content.
|
||||
Regression test for #3143 — Discord @mention-only messages.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": ""},
|
||||
]
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "(empty message)"
|
||||
|
||||
def test_whitespace_only_user_message_gets_placeholder(self):
|
||||
"""Whitespace-only user messages should also get placeholder."""
|
||||
messages = [
|
||||
{"role": "user", "content": " \n\t "},
|
||||
]
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
assert result[0]["content"] == "(empty message)"
|
||||
|
||||
def test_empty_user_message_list_gets_placeholder(self):
|
||||
"""Empty content list for user messages should get placeholder block."""
|
||||
messages = [
|
||||
{"role": "user", "content": []},
|
||||
]
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert len(result[0]["content"]) == 1
|
||||
assert result[0]["content"][0] == {"type": "text", "text": "(empty message)"}
|
||||
|
||||
def test_user_message_with_empty_text_blocks_gets_placeholder(self):
|
||||
"""User message with only empty text blocks should get placeholder."""
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": ""}, {"type": "text", "text": " "}]},
|
||||
]
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert result[0]["content"] == [{"type": "text", "text": "(empty message)"}]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build kwargs
|
||||
|
||||
Reference in New Issue
Block a user