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.
- The error names the offending block.
messages.17:andmessages.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_useblock plus an emptytextblock is the classic shape. - The final assistant message may be empty, but no other message may be. That asymmetry surprises people.
- Every
tool_useneeds a matchingtool_resultin 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.
| Message | Condition |
|---|---|
messages: text content blocks must be non-empty | A text block whose content is "" |
messages: text content blocks must contain non-whitespace text | A text block containing only spaces, tabs, or newlines |
system: text content blocks must contain non-whitespace text | The same thing in the top-level system parameter |
messages.17: all messages must have non-empty content except for the optional final assistant message | A message at index 17 with no content at all |
messages: final assistant content cannot end with trailing whitespace | A trailing space or newline on the last assistant turn |
messages: first message must use the "user" role | The array starts with an assistant turn |
messages: roles must alternate between "user" and "assistant", but found multiple "assistant" roles in a row | Consecutive same-role turns, on paths that enforce it |
max_tokens: 131072 > 128000, which is the maximum allowed number of output tokens for claude-opus-5 | max_tokens above the model ceiling |
prompt is too long: 206767 tokens > 200000 maximum | Input 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.
{
"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.
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
systemrole in the Messages API. System prompts go in the top-levelsystemparameter. 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.
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.
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 agent | What broke | Fix |
|---|---|---|
| Error after a user cancelled mid-turn | The tool_use shipped, the tool never ran | Append a synthetic tool_result with is_error: true before continuing |
| Error after a tool timed out | Same, with a longer gap | Same. Never drop the tool_use block instead: that changes history the model already saw |
| Error only on long conversations | A trimming step cut between a tool_use and its tool_result | Trim in pairs. Treat a tool exchange as one indivisible unit |
| Error on parallel tool calls | Results returned in separate messages | All results for one assistant turn go in a single following user message |
| Error after a crash and resume | The tool_use persisted, the result did not | On 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.
| Model | API ID | Context | Max output |
|---|---|---|---|
| Claude Fable 5 | claude-fable-5 | 1M | 128k |
| Claude Opus 5 | claude-opus-5 | 1M | 128k |
| Claude Sonnet 5 | claude-sonnet-5 | 1M | 128k |
| Claude Haiku 4.5 | claude-haiku-4-5-20251001 | 200k | 64k |
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.
- Claude API errors and common validation errors error envelope, prefill rejection, thinking parameter errors, size limits
- Claude Messages API reference role alternation, the absence of a system role, max_tokens semantics
- Claude context windows prompt-too-long behaviour and model_context_window_exceeded
- Claude models overview context and max output per model
- anthropics/claude-code issue 50010 reproduced non-empty-text-block 400