Troubleshooting · Request shape

400 Bad Request after twenty good turns — you're looking for a trigger, not a typo

30-SECOND VERSION
  1. Your configuration isn't wrong — it didn't change between turns. The request did. Look for the trigger, not the config.
  2. Minimal reproduction: strip the request to nothing, then add elements back one at a time. The first one that reproduces it is your trigger.
  3. 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

any of these
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 requestWorked for a while, then started failing
What changedyour configurationthe content of your request
Where to lookmodel name, endpoint path, required fieldswhich element just entered the conversation
Useful movecompare your payload against the documented shapeminimal repro (below)
Typical causewrong model, missing max_tokens, wrong API shapea 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 classWhat actually happensHow it behavesWho fixes it
1 — the message sequence became invalida tool_use with no matching tool_result, a tool_result with nothing to answer, two user messages in a row, an empty content blockDeterministic: same conversation, same turn, every time. Replaying the saved body reproduces it exactlyyou, or your client
2 — accumulated context crossed a boundarythe history grew past a limit — context window, request body size, total attachment bytesThreshold-shaped: fine until turn N, broken from turn N onward; deleting old messages makes it go awayyou — see context length exceeded
3 — the endpoint's support for one field is incompletethe endpoint accepts the basic shape but not one specific combination: streaming together with tools, thinking blocks, multimodal content partsAppears the moment that feature is used, and only then; the same conversation without it worksthe 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:

terminal
# 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.

terminal
jq '.messages | length' request.json      # how many messages went out
wc -c request.json                        # how big the body actually is
Strip the credential before sharing this file with anyone. A saved request body contains your key.

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:

terminal
jq -r '.messages[] | "\(.role)\t\([.content[]?.type] | join(" "))"' request.json | tail -20

You get something like:

output
user      text
assistant text tool_use
user      tool_result
assistant text

Now check four invariants against that output:

  • every line containing tool_use is immediately followed by a line containing tool_result,
  • no line contains tool_result unless the line above it contains tool_use,
  • user and assistant alternate,
  • 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:

terminal
# <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:

terminal
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.

StepAdd backIf it 400s here, the trigger is
1the system promptthe system prompt itself — length, or a block type the endpoint doesn't accept
2the tools array, declared but never calledthe tool schema, not the tool call
3one complete tool_use + tool_result round from the failing conversationtool-call round-tripping — go back to step 2's invariants
4stream: truestreaming combined with whatever is already in the payload
5thinking, if you have it enabledthinking-block handling
6an image or document blockmultimodal support
7the full message historynot 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:

terminal
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-streamingStreaming
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