400 Bad Request after twenty good turns — you're looking for a trigger, not a typo
- Your configuration isn't wrong — it didn't change between turns. The request did. Look for the trigger, not the config.
- Minimal reproduction: strip the request to nothing, then add elements back one at a time. The first one that reproduces it is your trigger.
- Three common triggers: unpaired
tool_use/tool_result, context creeping past the limit, or incomplete endpoint support for streaming/tools/multimodal.
A 400 that appears mid-conversation is a different problem from a 400 that appears on your first request. Your configuration did not change between turn 20 and turn 21 — your request did. Re-reading your config will find nothing, because there is nothing wrong with it. The job is to find the one element that turned an acceptable request into a rejected one, and there is a mechanical way to do that in about five minutes.
What you're seeing
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages: roles must alternate between \"user\" and \"assistant\", but found multiple \"user\" roles in a row"}}
400 Bad Request
{"type":"error","error":{"type":"invalid_request_error","message":"messages.3: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_xxxxxxxx. Each `tool_use` block must have a corresponding `tool_result` block in the next message."}}
{"type":"error","error":{"type":"invalid_request_error","message":"messages.4: `tool_result` block(s) provided when previous message does not contain any `tool_use` blocks"}}
All four are invalid_request_error. That means the request reached the server, was parsed, and was rejected as malformed — so the network, the credential and the model name are all fine, or you would be looking at a different status code. The problem is inside the body you sent.
First, split the question
| Fails on the first request | Worked for a while, then started failing | |
|---|---|---|
| What changed | your configuration | the content of your request |
| Where to look | model name, endpoint path, required fields | which element just entered the conversation |
| Useful move | compare your payload against the documented shape | minimal repro (below) |
| Typical cause | wrong model, missing max_tokens, wrong API shape | a tool call round, a thinking block, an image, sheer size |
The rest of this page is about the right-hand column. If your very first request 400s, you have a plain configuration problem and the fastest fix is a field-by-field comparison against the endpoint's documented request shape.
The three things that trigger a mid-conversation 400
| Trigger class | What actually happens | How it behaves | Who fixes it |
|---|---|---|---|
| 1 — the message sequence became invalid | a tool_use with no matching tool_result, a tool_result with nothing to answer, two user messages in a row, an empty content block | Deterministic: same conversation, same turn, every time. Replaying the saved body reproduces it exactly | you, or your client |
| 2 — accumulated context crossed a boundary | the history grew past a limit — context window, request body size, total attachment bytes | Threshold-shaped: fine until turn N, broken from turn N onward; deleting old messages makes it go away | you — see context length exceeded |
| 3 — the endpoint's support for one field is incomplete | the endpoint accepts the basic shape but not one specific combination: streaming together with tools, thinking blocks, multimodal content parts | Appears the moment that feature is used, and only then; the same conversation without it works | the endpoint operator |
Class 1 and class 3 are indistinguishable from the console — both are just 400. Only a minimal repro separates them. Building one is also the only way to file a report an operator can act on.
Check in this order
1 · Save the request body that failed#
Not the console line — the console line is truncated and often reformatted. You need the JSON that actually went out.
Two ways, in order of reliability:
# a) whatever your client calls its verbose / debug log, turn it on and find the outgoing body
# then save it as request.json
# b) if the client won't show it, put a logging proxy in front and re-run the failing turn
export ANTHROPIC_BASE_URL=http://127.0.0.1:8080
Once you have request.json, you have a reproducible artifact. Everything below operates on it, not on the running client.
jq '.messages | length' request.json # how many messages went out
wc -c request.json # how big the body actually is
2 · Read the shape before you read the content#
Most mid-conversation 400s are visible in the structure of the message array, and you can see the whole structure in one screen:
jq -r '.messages[] | "\(.role)\t\([.content[]?.type] | join(" "))"' request.json | tail -20
You get something like:
user text
assistant text tool_use
user tool_result
assistant text
Now check four invariants against that output:
- every line containing
tool_useis immediately followed by a line containingtool_result, - no line contains
tool_resultunless the line above it containstool_use, userandassistantalternate,- no line has an empty block list.
The first violation you find is very likely your 400. This is the single highest-yield check on the page, and it takes one command.
3 · Strip it to the smallest thing that works#
Establish a baseline against the same endpoint and the same model:
# <model-id>: copy an id from https://api.9coding.com/v1/models
curl -sS -w '\n%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<model-id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
If this 200s, your baseline is good. If it doesn't, stop — you don't have a mid-conversation problem, you have a configuration problem, and nothing below applies.
Replaying the saved body uses the same command with the file:
curl -sS -w '\n%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
--data-binary @request.json
4 · Add elements back one at a time#
Start from the baseline and put back exactly one thing per attempt. The first element that makes the 400 come back is your trigger — stop there. You do not need to understand the whole conversation, only that one element.
| Step | Add back | If it 400s here, the trigger is |
|---|---|---|
| 1 | the system prompt | the system prompt itself — length, or a block type the endpoint doesn't accept |
| 2 | the tools array, declared but never called | the tool schema, not the tool call |
| 3 | one complete tool_use + tool_result round from the failing conversation | tool-call round-tripping — go back to step 2's invariants |
| 4 | stream: true | streaming combined with whatever is already in the payload |
| 5 | thinking, if you have it enabled | thinking-block handling |
| 6 | an image or document block | multimodal support |
| 7 | the full message history | not the shape — size. Go to step 6 below |
Keep each attempt as a separate file (try-1.json, try-2.json) so you can hand the failing one to whoever needs it.
5 · Bisect the history — but cut on a legal boundary#
If nothing above reproduces it and only the full history does, halve the array and replay:
jq '.messages |= .[-6:]' request.json > try-history.json
There is a trap here that costs people an afternoon: an arbitrary cut can create a brand-new 400. Slice in the middle of a tool round and you orphan a tool_result; slice so the array starts with an assistant message and you break alternation. Then you're debugging your own trimming, not the original bug.
Cut only where the first remaining message is a user message that contains no tool_result block. Re-run the step 2 command on every trimmed file before you conclude anything from it.
6 · Decide whether it's a shape problem or a size problem#
They need different pages, and they are easy to tell apart:
- Shape — it fails at the same turn even after you delete unrelated older messages; the message names a field, an index (
messages.3), or a block type. - Size — trimming any old messages makes it work; it breaks at roughly the same point in every long conversation; the text mentions maximum context or a prompt being too long. That's a different problem with a different fix. See context length exceeded.
When it isn't your problem
If the minimal repro from step 4 is small — a plain message with tools declared, or one tool round, or one image — and it still 400s, the shape is not the issue. Support for that field is.
Four minutes of curl gets you a support matrix that no documentation will give you. Test each cell against your endpoint and write down what you get:
| Non-streaming | Streaming | |
|---|---|---|
| plain message | ||
+ tools declared | ||
+ one tool_use / tool_result round | ||
| + thinking | ||
| + image block |
An endpoint can accept the basic message shape and still be incomplete one column over — support is not a single yes or no, it has a grain to it. Once a cell fails, you're done debugging: send the operator the smallest failing body and the status code. That is a report they can act on, and it's the reason the repro was worth building.
Check the operator's status page and changelog before assuming it was always broken — a field that worked yesterday and fails today is an incident, not a limitation. Ours is at status.
When reporting it, include the request id — 9Coding error responses carry one in the form (request id: 2026...). That id plus the timestamp, the model name and the full error text lets the exact call be traced. A report that only says it doesn't work cannot be investigated.
A malformed request and a conversation that outgrew the window are not the same problem
A 400 invalid_request_error means the body you sent is not a legal request: something in the message array violates a rule, and no amount of trimming history fixes the rule violation. Running out of room is the opposite — the request is perfectly well formed and there is simply too much of it, so trimming is the entire fix. The tell is whether deleting unrelated old messages changes anything. If it does, you're on the wrong page; see context length exceeded.
Related
- context length exceeded — when it's size, not shape
- 401 Unauthorized — two layers of authentication, one error message
- Connection errors — separating network, proxy, and endpoint
- 429 Too Many Requests — upstream capacity, and what retrying actually does
- All Claude Code errors — the quick reference table