What you will be able to do
- Read a 400 message as a pointer to the integration code that built the bad request
- Use stop_reason and stream error events to find silent failures that return HTTP 200
- Diagnose an empty end_turn response and fix it in the message structure
1.When a 400 points into your own message history
Debugging a Claude application starts with deciding whether the integration or the model is at fault, and a 400 invalid_request_error settles that early: the request was rejected before the model produced anything, so the bug is in what your code sent. The harder part is finding which part of the request is wrong, and for the message and thinking structures that tool-use loops build, the error text usually says.
In tool-use and thinking flows, the 400 message is specific enough to point you at the code to fix. These are the ones integrations hit most often:
- tool_use ids were found without tool_result blocks immediately after: a tool_use id got no tool_result, or text was placed before the results. Return one tool_result for each tool_use, with the results first.
- thinking or redacted_thinking blocks in the latest assistant message cannot be modified: your code edited, reordered or filtered thinking blocks before sending them back. The message starts with the position of the bad block, such as messages.1.content.0. Send the assistant message back unchanged.
- This model does not support assistant message prefill: the conversation ends with an assistant message, and this model rejects prefill. End with a user message, and control the output format with structured outputs or system prompt instructions.
- A message ending but no web_search tool was provided: a resume request left out a server tool that the previous turn is still waiting on. Keep the same tools array.
Thinking settings fail the same way. If you send a thinking.type the model doesn't accept, such as enabled to a model that no longer supports extended thinking, you get a 400 whose message names the rejected value. So when every call starts failing right after a model upgrade, the cause is usually a request setting that needs updating, not an outage.
Checkpoint 1 of 5· Check yourself
After a refactor, every follow-up request after a tool call fails with a 400. The message begins messages.1.content.0 and says thinking blocks cannot be modified. Where is the fault?
This 400 means your application changed the thinking blocks before sending them back. The fix is to return the whole assistant message unchanged and then add the tool_result.
“your application is altering the assistant's thinking blocks before sending them back.”Source: platform.claude.com
2.Following stop_reason when nothing throws
Silent failures are harder to find: HTTP 200, no exception, wrong result. Check the connection first. A streaming response can fail after the 200 arrives, as an error event inside the server-sent event stream. A handler that only checks the initial status will log the failed call as a success. Then check the response itself. Every successful Messages API response includes [stop_reason](lesson:6.3), which records why the model stopped generating. That one field usually tells you which side owns the problem.
Each stop_reason value points to a different owner and a different fix. refusal means the model declined to respond. Your integration sent the request correctly, so don't look for a bug: read stop_details and retry on a fallback model. max_tokens means your own limit cut the output off. You see text that stops mid-sentence, or a last tool_use block that is incomplete and needs a retry with a higher max_tokens. model_context_window_exceeded also means the output was cut off. Only end_turn says the model finished on purpose.
Checkpoint 2 of 5· Fill the gap
Which stop_reason value does this truncation check test for?
# Check if response was truncated during tool use
if response.stop_reason == " ? ":
# Check if the last content block is an incomplete tool_use
last_block = response.content[-1]
if last_block.type == "tool_use":A response cut off at the max_tokens limit can end with an incomplete tool_use block. The fix is to resend with a higher max_tokens.
Source: platform.claude.comSome behavior that looks like the model's own choice actually comes from the integration. Suppose Claude won't act on a tool result, or asks the user to confirm instructions it found there. Check whether your code is putting its own instructions inside the tool_result content. Claude treats instructions inside tool results as possibly untrusted content from a third party. Keep the tool result to just the data. Send your instructions in a user turn after the tool_result block, or, on models that support it, in a system message in the middle of the conversation. Exact string comparison on tool inputs is another example: it can break when the model version changes, because escaping of Unicode and forward slashes differs between versions. Parse the JSON instead.
Checkpoint 3 of 5· Check yourself
All of these return HTTP 200 with no exception. Which one points to a bug in the integration rather than a model decision to handle?
Claude is trained to treat instructions inside tool results as untrusted, so the fix is to move your instructions out of the tool result. A refusal is the model declining, and you handle it with a fallback model, not a code fix.
“Claude is trained to treat instructions inside tool results as potentially untrusted third-party content.”Source: platform.claude.com
Checkpoint 4 of 5· Exam question
A team's usage sits well under their published requests-per-minute limit, yet they begin seeing 429 errors after a marketing campaign causes API traffic to triple within a few minutes. Historical usage was steady before the spike. What is the most likely explanation, and what should the team change to avoid recurrence?
Correct answer: A — The spike likely triggered acceleration limits designed to catch sharp usage increases; the team should ramp traffic up gradually and keep usage patterns more consistent
- A. Correct. The documentation notes that a sharp increase in usage can trigger 429 errors from acceleration limits even while under the standard published limits, and the recommended mitigation is to ramp traffic up gradually and maintain consistent usage patterns.
- B. Published RPM limits apply per model to the Messages API itself; the Message Batches API has its own separate rate limits, so this statement misattributes which endpoint the published limit governs.
- C. A 429 rate_limit_error does not indicate key revocation; a revoked or invalid key produces a 401 authentication_error instead, so this diagnosis points to the wrong error type.
- D. Exceeding the maximum request payload size produces a 413 request_too_large error, not a 429, and is unrelated to the number of requests sent per minute.
3.Empty end_turn responses
An empty end_turn shows up in the model's output, but the integration usually causes it. The response is exactly 2–3 tokens with no content and stop_reason: "end_turn", and it usually comes right after tool results. The most common cause is a habit many integrations have: adding a text block after the tool_result in the same user message, such as a friendly Here's the result. Claude learns that the user always adds text after tool results, so it ends its turn and waits for that text. Nothing throws, the status is 200, and the stop reason reports a normal finish. That is why this failure can go unnoticed in production for a long time.
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"},
{
"type": "text",
"text": "Here's the result", # Don't add text after tool_result
},
],
},The fix is in the message structure: send a user message that contains only the tool_result blocks. The second cause is sending Claude's finished response back with nothing added. That rules out the obvious workaround: retrying with the empty response unchanged won't help, because Claude has already decided it is done. If you still get empty responses after fixing the message structure, add a continuation prompt as a new user message. Use this only as a last resort, not as the first fix.
def handle_empty_response(client, messages):
response = client.messages.create(
model="claude-opus-5-5", max_tokens=1024, messages=messages
)
# Check if response is empty
if response.stop_reason == "end_turn" and not response.content:
# INCORRECT: Don't just retry with the empty response
# This won't work because Claude already decided it's done
# CORRECT: Add a continuation prompt in a NEW user message
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-5-5", max_tokens=1024, messages=messages
)
return responseCheckpoint 5 of 5· Put it in order
Put the steps for fixing recurring empty end_turn responses in order
- 1.Send the request again with that added message
- 2.Remove any text blocks that follow tool_result blocks in the user message
- 3.Detect a response with stop_reason end_turn and no content
- 4.If responses are still empty, append a new user message with a continuation prompt
Fix the message structure first. Add a continuation prompt in a new user message only if the structure fix doesn't solve it.
“Use continuation prompts as a last resort: Only if these fixes don't resolve the issue.”Source: platform.claude.com
Sources3
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.An empty end_turn response is a one-off model glitch, so resending the same conversation will fix it.Why is that wrong?
Claude has already decided the turn is done. Fix the message structure first, and only then add a continuation prompt in a new user message.
Covered in Empty end_turn responses
2.A refusal stop_reason on a 200 response means the integration sent a broken request.Why is that wrong?
A refusal means the model declined, even though the request was processed successfully. Handle it by reading stop_details and retrying on a fallback model.
Covered in Following stop_reason when nothing throws
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Missing tool_result for some tool_use ids, or tool_result is not the first content block in the user message”
↩︎ When a 400 points into your own message history“Unicode and forward-slash escaping differs between model versions”
↩︎ Following stop_reason when nothing throws“your application is altering the assistant's thinking blocks before sending them back.”
↩︎ Checkpoint“Claude is trained to treat instructions inside tool results as potentially untrusted third-party content.”
↩︎ Checkpoint - 2.https://platform.claude.com/docs/en/api/errorsOfficial docs
“The error message starts with the position of the offending block (for example, messages.1.content.0) and contains:”
↩︎ When a 400 points into your own message history“This model does not support assistant message prefill. The conversation must end with a user message.”
↩︎ When a 400 points into your own message history“an error can occur after the API returns a 200 response.”
↩︎ Following stop_reason when nothing throws - 3.
“A resume request that no longer defines the waiting server tool fails with a 400”
↩︎ When a 400 points into your own message history“Read stop_details and retry on a fallback model.”
↩︎ Following stop_reason when nothing throws“you'll need to retry the request with a higher max_tokens value to get the full tool use.”
↩︎ Following stop_reason when nothing throws“Sometimes Claude returns an empty response (exactly 2–3 tokens with no content) with stop_reason: "end_turn".”
↩︎ Empty end_turn responses“Claude learns to expect the user to always insert text after tool results, so it ends its turn to follow the pattern”
↩︎ Empty end_turn responses“add a continuation prompt in a new user message rather than retrying with the empty response”
↩︎ Exam trap 1“Unlike errors, which indicate failures in processing your request, stop_reason tells you why Claude completed its response generation.”
↩︎ Exam trap 2“Sending Claude's completed response back without adding anything (Claude already determined it's done, so it will remain done)”
↩︎ Prediction“Use continuation prompts as a last resort: Only if these fixes don't resolve the issue.”
↩︎ Checkpoint - 4.
“Most thinking configuration errors are a mismatch between the thinking.type value in the request and what the model supports.”
↩︎ When a 400 points into your own message history