Skip to content

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages: text content blocks must contain non-whitespace text"},"request_id":"req_011CXnuVCF3KmwzeqdM4gFNZ"}

Somewhere in the conversation you are sending, a text content block contains an empty string, or a string made only of spaces, tabs or newlines. The request was rejected at validation, before any model ran — so this is not your API key, not your credit balance, not capacity, and not the model refusing the task. It is also not a context-length problem, which is the one people guess when a long-running session suddenly stops working: a full context returns Prompt is too long, not this.

The message that refuses to tell you where

Look at what this message does not have. Its siblings in the same family point straight at the offender — messages.71.content.8, tools.48.custom.name. This one says messages: and stops. The check is applied across the array rather than reported per element, so the API knows exactly which block is empty and does not tell you. Plan accordingly: the work here is location, not diagnosis. The diagnosis is already in the sentence.

The second thing worth internalising is that the Messages API is stateless. There is no conversation on the server. Your client re-sends the entire history as one array on every turn, and the array is validated from the top each time. An empty block that entered the history three turns ago is in every request you make from then on, which is why this reads as a session that has died rather than a request that failed. The request is a symptom; the transcript is the patient.

Everything else about this being a 400 invalid_request_error — that no model ran, that you were not billed for output, that the failure is deterministic — is spelled out on the request-body parse failure page, which sits one layer earlier in the same pipeline. Worth reading once for the whole family rather than five times.

Is retrying useful?

No. The array is invalid, and pressing Enter again sends the same array.

The official SDKs retry connection errors, rate limits and 5xx responses with exponential backoff; 400 is excluded on purpose, and that is correct behavior rather than an oversight. Waiting does not populate an empty string.

Retry once if you want proof that it is deterministic — the second identical failure is the confirmation, and there is nothing to learn from a third. Then stop. If you have your own retry wrapper around this call, exclude 400 from it before you go looking for the block, or you will be re-uploading the entire conversation on a timer for as long as you spend debugging.

Where an empty text block comes from when you never wrote one

Nobody types an empty message. These blocks get manufactured, and the four sources are distinguishable by when the failure started.

  • An assistant turn the API itself produced. This is the one that catches people, and it is worth stating plainly: a response can legally contain a text block that a request may not. An assistant turn that goes almost straight to a tool call can carry a text block holding nothing but a newline. If your client stores assistant turns verbatim and replays them — which is the correct, lossless thing to do — you will eventually replay a block the input validator rejects. The output grammar is looser than the input grammar. Test: the failure began on the turn right after a model response that had little or no visible prose before a tool call.
  • A tool result that came back empty. A command that printed nothing, a file that is zero bytes, a search with no hits, a script whose output went to stderr. If your integration maps tool output into a text block without checking for emptiness, an empty result becomes an illegal block. Test: look at the last tool that ran before the first failure and check whether it produced any stdout at all.
  • Your own preprocessing stripped the content. A redaction pass, a secret scrubber, a Markdown sanitizer, a template that rendered to nothing because a variable was undefined. These leave the block in place and empty it. Test: the same conversation works when the preprocessing step is disabled.
  • An interrupted or cancelled stream. The client opened a text block, wrote nothing into it, and stored it when the turn was abandoned. Test: the failure began immediately after an Esc, a crash, or a cancelled request.

If you cannot tell which, bisect. Send the first half of the history as a standalone request; whichever half fails contains the block. Two or three splits is usually enough, and it is faster than reading a long transcript by eye.

Mechanically, the offender is easy to recognise once you are looking at JSON: a content entry of type text whose string is empty or matches whitespace only. For a CLI session the history is a JSONL file per session under ~/.claude/projects/<project>/, one JSON object per line, so a whitespace-only text value is greppable. If you build the request yourself, dump the serialized body just before it is sent and check every text block in it — including the ones inside assistant messages you are replaying, which is where people forget to look.

The fix is to drop the block, not to trim it

The instinctive repair is to trim the string, and trimming is exactly what produces the error. A block holding "\n" becomes a block holding "", which is still illegal. A block holding " hello " was never the problem.

Remove the whole block from the content array. If that leaves a message with no content blocks at all, remove the message. Apply it in the place that suits your case:

  • Replaying assistant turns — filter empty text blocks out at the point where you append a response to history, not at send time. Filtering at send time means the stored transcript stays broken and every tool that reads it inherits the bug.
  • Tool results — never map empty output into a text block. Write something the model can act on instead: that the command produced no output, or that the file is empty. That is more useful than an empty block would have been even if the API accepted it.
  • Preprocessing — make the pass delete blocks it empties, rather than emptying them in place.
  • Inside a CLI session you cannot edit — you cannot repair the history from within the session, because the request that would carry your edit is the request being rejected. Rewind to a checkpoint before the poisoned turn, or start a new session and restate the task. Note that starting a new session is a workaround, not a fix: whatever manufactured the block will do it again.

Knowing the history is clean again

Do not re-run the task that broke, and do not accept one success as the answer — a short turn can miss the bad block entirely and look like a cure.

Send a turn that carries the full conversation and calls no tools: a plain question. If it returns normally, every block in the history passed validation, which is precisely the property that was failing. Then run one turn that exercises the thing that created the empty block in the first place — the tool that returned nothing, the preprocessing step, the model response with no prose — and confirm it now either produces content or produces no block. If you only tested with content present, you have not tested the fix.

If the 400 complains about JSON rather than about blocks — a character offset instead of a field name — the body never parsed at all, and a single broken character is poisoning every turn; same permanence, and the location strategy there is completely different. If it names thinking or redacted_thinking blocks in the latest assistant message, you have hit the opposite rule: those blocks may not be altered at all, which is worth knowing before you write a filter that rewrites assistant turns. And if it names tool use concurrency and points you at /rewind, the break is a missing tool_result rather than an empty block. All three are stored-history failures with the same shape; the AI coding error triage tool separates them by what the message points at, which here is nothing at all.