CertSafari
    Anthropic Claude Certified Developer – Foundations (CCDV-F)· Lessons

    Domain 4 · Lesson 11/25

    Tracing Claude failures to the integration or the model

    Debugging and Error Handling

    9 min read
    2.6% of exam
    4 sources
    Published 12 Oct 2026
    Docs as of 30 Sep 2026

    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?

    Sources1234

    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":

    Some 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?

    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?

    Sources231

    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.

    The integration mistake behind most empty end_turn responses: a text block after the tool_resultpython
        {
            "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.

    Last-resort handling: detect the empty end_turn and add a continuation prompt in a new user messagepython
    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 response

    Checkpoint 5 of 5· Put it in order

    Put the steps for fixing recurring empty end_turn responses in order

    1. 1.Send the request again with that added message
    2. 2.Remove any text blocks that follow tool_result blocks in the user message
    3. 3.Detect a response with stop_reason end_turn and no content
    4. 4.If responses are still empty, append a new user message with a continuation prompt

    Sources3

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 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. 2.
      “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. 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. 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

    Ready to test yourself?

    Practise the 20 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.