API error: 400 messages: text content blocks must be non-empty

An empty text block is almost never something you typed. It is something a framework produced: a tool call with no accompanying text, a trimmed assistant turn, a replayed message from a store that dropped its content. The fix is a filter at the boundary, not a patch at the call site.

By the Continuum team. We build a workbench that runs Claude Code, Codex, and their peers, so the model rates quoted here are the ones our own cost analytics ship with.

The short version

messages: text content blocks must be non-empty is a 400 invalid_request_error from the Claude API, raised when any text block in your messages array has an empty string as its content. Almost every real occurrence comes from framework code rather than hand-written requests: an assistant turn that carried only a tool call, a message replayed from a transcript store that persisted the tool blocks but not the text, or a trimming step that stripped a message to nothing. There is a stricter sibling for whitespace-only blocks, a separate rule about trailing whitespace on the final assistant message, and a set of tool-block ordering rules with their own messages. Fixing them one call site at a time does not work; filtering the array once before it leaves your process does.

What you need to know
  • The error names the offending block. messages.17: and messages.0.content.0: prefixes are a path, not decoration.
  • An empty string fails, and so does a single space: that one produces text content blocks must contain non-whitespace text.
  • Tool-only assistant turns are the usual source. A tool_use block plus an empty text block is the classic shape.
  • The final assistant message may be empty, but no other message may be. That asymmetry surprises people.
  • Every tool_use needs a matching tool_result in the very next message, and vice versa.
  • Prefill is rejected on Claude 4.6 and later. Code that ends the array with an assistant turn to steer output now 400s.
  • Sanitize once, at the boundary. Drop empty blocks, drop messages left with no blocks, then send.

messages: text content blocks must be non-empty

The full response is a 400 with the standard Anthropic error envelope. Nothing about it is ambiguous once you know that the prefix before the colon is a path into the request you sent.

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages: text content blocks must be non-empty"
  }
}

Anthropic does not publish a catalogue of these strings, so the table below is assembled from reproduced API responses in public issue reports, checked August 2026. The wording is exact; the index numbers vary with your request.

The invalid_request_error strings you are most likely to hit, and what causes each.
MessageCondition
messages: text content blocks must be non-emptyA text block whose content is ""
messages: text content blocks must contain non-whitespace textA text block containing only spaces, tabs, or newlines
system: text content blocks must contain non-whitespace textThe same thing in the top-level system parameter
messages.17: all messages must have non-empty content except for the optional final assistant messageA message at index 17 with no content at all
messages: final assistant content cannot end with trailing whitespaceA trailing space or newline on the last assistant turn
messages: first message must use the "user" roleThe array starts with an assistant turn
messages: roles must alternate between "user" and "assistant", but found multiple "assistant" roles in a rowConsecutive same-role turns, on paths that enforce it
max_tokens: 131072 > 128000, which is the maximum allowed number of output tokens for claude-opus-5max_tokens above the model ceiling
prompt is too long: 206767 tokens > 200000 maximumInput alone exceeds the context window

Where empty content blocks actually come from

Nobody writes {"type": "text", "text": ""} on purpose. Every occurrence is a byproduct, and there are four common generators.

1. The tool-only assistant turn

This is the big one. When Claude answers with a tool call and no prose, the assistant turn contains a tool_use block and nothing else. Frameworks that model an assistant message as "text plus optional tool calls" reconstruct it with an empty string in the text slot, and it round-trips fine right up to the moment you replay that history into a follow-up request.

The shape that gets rebuilt wrong. The second block is the one the API rejects.
{
  "role": "assistant",
  "content": [
    { "type": "tool_use", "id": "toolu_01A...", "name": "read_file",
      "input": { "path": "src/main.py" } },
    { "type": "text", "text": "" }
  ]
}

2. Replayed transcripts

Any system that persists conversations and rehydrates them later can lose content in the round trip: a database column that trims, a JSON serializer that writes null for an empty list, a summarization pass that replaces a message body with its summary and forgets one branch. The failure is not at write time, when the record still looks plausible, but on the next request that replays it. The index in messages.17: is your fastest route to the culprit.

3. Trimming and context management

Context-window management that truncates messages will eventually truncate one to zero characters. Same for a redaction step that strips secrets from tool output, or a filter that removes thinking blocks and leaves the container behind. A whitespace-preserving trimmer produces the subtler variant, where a message survives as a single newline and earns you must contain non-whitespace text instead.

4. Streaming that ended early

A stream interrupted by an overload or a dropped connection can leave a partially materialized assistant message with an empty text block. Retrying by appending to that fragment sends the empty block straight back to the API. Discard incomplete turns rather than repairing them. This is the direct link between a 529 and a 400 that shows up an hour later.

The fix: sanitize once, at the boundary

Patching each call site fails because there are more call sites than you think and new ones arrive with every feature. Put one function between your message store and the API, and make it the only path.

Handles the empty case, the whitespace case, the emptied-message case, and the trailing-whitespace rule.
def sanitize(messages):
    out = []
    for msg in messages:
        content = msg["content"]
        if isinstance(content, str):
            if content.strip():
                out.append({"role": msg["role"], "content": content})
            continue

        blocks = []
        for b in content:
            # text blocks must be non-empty AND non-whitespace
            if b.get("type") == "text" and not b.get("text", "").strip():
                continue
            blocks.append(b)

        # a message emptied by that filter must not be sent at all
        if blocks:
            out.append({"role": msg["role"], "content": blocks})

    # the final assistant turn may not end in whitespace
    if out and out[-1]["role"] == "assistant":
        last = out[-1]["content"]
        if isinstance(last, list) and last and last[-1].get("type") == "text":
            last[-1]["text"] = last[-1]["text"].rstrip()

    return out

A second guard worth adding at the same boundary: assert that the array is non-empty and starts with a user turn. Both are cheap, both catch a real class of bug, and both produce a stack trace pointing at your code instead of a 400 pointing at an index.

Role alternation, and why the docs and the errors disagree

This is worth stating carefully, because the honest answer is more useful than the confident one. Anthropic's Messages API reference says:

And yet roles must alternate between "user" and "assistant" and first message must use the "user" role are both still being reported in July and August 2026. The pattern in those reports is that they arrive through Bedrock, Vertex, or a proxy rather than through api.anthropic.com directly. The most defensible reading: the first-party API merges consecutive turns, and other serving paths enforce strict alternation.

  • Always alternate, always start with a user turn. Treat it as a portability requirement, not a first-party rule. It costs nothing and it is the difference between code that survives a move to Bedrock and code that does not.
  • There is no system role in the Messages API. System prompts go in the top-level system parameter. A message with "role": "system" is a 400, and it is a very common porting bug coming from an OpenAI-shaped codebase.
  • Merge before you send if your agent naturally produces consecutive user turns, for example a user message plus an injected context block. Concatenate them into one message rather than relying on the server to do it.

Tool blocks: the pairing rules and their exact errors

Tool use adds two hard structural rules, and the API states them inside the error messages themselves rather than in a reference table. Both are worth quoting because the wording tells you precisely what to check.

A tool_result with nothing to attach to.
messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks:
toolu_01SH5hUP1jreQezT5q4R4yQh. Each `tool_result` block must have a
corresponding `tool_use` block in the previous message.
A tool_use whose result never came back.
messages.216: `tool_use` ids were found without `tool_result` blocks immediately
after: tooluse_NudlJpcjeQemU5wudkIYMA. Each `tool_use` block must have a
corresponding `tool_result` block in the next message.
Symptom in your agentWhat brokeFix
Error after a user cancelled mid-turnThe tool_use shipped, the tool never ranAppend a synthetic tool_result with is_error: true before continuing
Error after a tool timed outSame, with a longer gapSame. Never drop the tool_use block instead: that changes history the model already saw
Error only on long conversationsA trimming step cut between a tool_use and its tool_resultTrim in pairs. Treat a tool exchange as one indivisible unit
Error on parallel tool callsResults returned in separate messagesAll results for one assistant turn go in a single following user message
Error after a crash and resumeThe tool_use persisted, the result did notOn resume, scan for unpaired ids and close them before the next call

max_tokens, context, and the parameters that started 400ing

The remaining 400s are parameter problems, and two of them changed recently enough that working code can start failing without you touching it.

Output ceilings and context windows.checked aug 2026
ModelAPI IDContextMax output
Claude Fable 5claude-fable-51M128k
Claude Opus 5claude-opus-51M128k
Claude Sonnet 5claude-sonnet-51M128k
Claude Haiku 4.5claude-haiku-4-5-20251001200k64k

A max_tokens above the model ceiling is a hard 400 naming both numbers. Input that alone exceeds the context window is prompt is too long: N tokens > M maximum. What changed is the overlap case: on Claude 4.5 and newer, input plus max_tokens exceeding the window is accepted, and generation simply stops with stop_reason: "model_context_window_exceeded" if it gets there. Older models returned a validation error instead. Code written against the old behaviour that reserved headroom defensively is not wrong, just no longer necessary.

Thinking configuration has its own generation-specific rejections, each of which tells you what to use instead: "thinking.type.enabled" is not supported for this model on 4.7 and later, and adaptive thinking is not supported on this model on 4.5 and earlier. If you support more than one model generation, resolve thinking parameters from a per-model table rather than a single global default.

Questions people ask

What does "text content blocks must be non-empty" mean?

It is a 400 invalid_request_error from the Claude API telling you that one of the text blocks in your messages array has an empty string as its content. The API rejects the whole request rather than skipping the block. A single space fails too, with the slightly different message "text content blocks must contain non-whitespace text".

Why does my agent framework send empty content blocks?

Most often because an assistant turn contained only a tool call. Frameworks that model a message as text plus optional tool calls reconstruct that turn with an empty string in the text slot, which is fine locally and fatal when the history is replayed into the next request. Replayed transcripts, over-aggressive trimming, and interrupted streams are the other three common sources.

What does messages.17 mean in the error?

It is a path into the request you sent. Index 17 of your messages array is the offending message, and a longer form like messages.0.content.0 points at the first content block of the first message. Log your outgoing array with indices and the error tells you exactly which record in your store is bad.

Can the last message be an empty assistant turn?

The empty-content rule exempts the final assistant message, which is why the error says "except for the optional final assistant message". But on Claude 4.6 and later, assistant prefill is rejected outright with "This model does not support assistant message prefill. The conversation must end with a user message." So on current models the conversation should end with a user turn regardless.

Do Claude messages have to alternate between user and assistant?

The Messages API reference says consecutive same-role turns are combined into a single turn, yet roles-must-alternate 400s are still reported through Bedrock, Vertex, and proxy paths. Treat strict alternation starting with a user turn as a portability requirement. Note also that there is no system role: system prompts go in the top-level system parameter.

How do I fix "final assistant content cannot end with trailing whitespace"?

Right-strip the text of the last block of the final assistant message before sending. It usually comes from string concatenation that adds a trailing newline, or from a template that ends with a blank line. Do it in the same sanitizer that drops your empty blocks, since both bugs come from the same layer.

Sources

Every figure above was read from these pages on August 2026. Vendors reprice without notice; if you find a stale number, tell us.

  1. Claude API errors and common validation errors error envelope, prefill rejection, thinking parameter errors, size limits
  2. Claude Messages API reference role alternation, the absence of a system role, max_tokens semantics
  3. Claude context windows prompt-too-long behaviour and model_context_window_exceeded
  4. Claude models overview context and max output per model
  5. anthropics/claude-code issue 50010 reproduced non-empty-text-block 400
Try it

See the request,
not the red line.

Continuum drives Claude Code, Codex, and peers under your own subscriptions, with live quota gauges and spend by repo. The app is free. Mac is stable; Windows and Linux desktop are beta.

free app · your subscriptions · local-first