Skip to content

Bug: Empty text blocks in assistant messages cause 400 API errors #210

@gramanoid

Description

@gramanoid

Description

When Claude responds with tool calls, the assistant messages are stored with empty text blocks like {"type": "text", "text": ""} alongside the toolCall blocks. When these messages are later sent back to the Claude API as conversation history, Claude rejects them with:

400 messages.X: all messages must have non-empty content except for the optional final assistant message

Steps to Reproduce

  1. Start a conversation via WhatsApp
  2. Send a message that triggers Claude to use tools (e.g., "hi" which triggers reading context files)
  3. Send 2-3 more messages
  4. Eventually get the 400 error

Example Session Content

The stored assistant message looks like:

{
  "type": "message",
  "message": {
    "role": "assistant",
    "content": [
      {"type": "text", "text": ""},  // <-- This empty text block causes the issue
      {"type": "toolCall", "id": "toolu_xxx", "name": "read", "arguments": {...}}
    ]
  }
}

Root Cause

Claude's API responses include empty text blocks when the model goes directly to tool use. These are stored verbatim in the session JSONL files. When the conversation history is sent back to Claude, these empty text blocks violate the API's message content requirements.

Suggested Fix

Filter out empty text blocks when storing assistant messages to session files. Something like:

// Before storing assistant message
message.content = message.content.filter(block => 
  block.type !== 'text' || (block.text && block.text.length > 0)
);

Or alternatively, filter them when loading the conversation history before sending to the API.

Current Workaround

I'm running a real-time session cleaner service using inotifywait that monitors session files and removes empty text blocks immediately:

jq -c '
  if .type == "message" and .message.role == "assistant" then
    .message.content |= [.[] | select(
      (.type != "text") or ((.text // "") != "")
    )] |
    if (.message.content | length) > 0 then . else empty end
  else
    .
  end
' session.jsonl

Environment

  • ClawdBot via WhatsApp surface
  • Claude Haiku 4.5 model
  • Using CLIProxyAPI for OAuth

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions