Skip to content

[BUG] API errors are returned as text messages instead of raised as exceptions #472

@wayne-yeung-wy

Description

@wayne-yeung-wy

API errors are returned as text messages instead of raised as exceptions

Summary

API errors from the Anthropic API (including 400 Bad Request, 401 Authentication, 429 Rate Limit, 529 Overloaded, etc.) are being returned as text messages in the chat stream instead of being raised as exceptions. This makes it impossible for applications to programmatically handle API errors.

Impact

Critical: Applications cannot catch and handle API errors, leading to:

  • Errors appearing as chat messages to end users
  • No ability to implement retry logic for transient errors (529 Overloaded)
  • No way to detect authentication failures programmatically
  • Invalid configurations (wrong model names) appearing as chat responses

Reproduction

Example 1: Invalid Model Identifier

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def test_invalid_model():
    options = ClaudeAgentOptions(
        model="claude-opus-4-5-20251101"  # Invalid model name
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Hello")

        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                print(message.content[0].text)
                # Prints: "API Error (claude-opus-4-5-20251101): 400 The provided model identifier is invalid."
                # Expected: anthropic.BadRequestError exception

Example 2: API Overload (529)

async def test_overload():
    async with ClaudeSDKClient() as client:
        await client.query("Some request during high load")

        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                print(message.content[0].text)
                # Prints: "API Error: Repeated 529 Overloaded errors"
                # Expected: anthropic.RateLimitError or similar exception

Expected Behavior

API errors should be raised as exceptions, similar to how the Anthropic Python SDK works:

try:
    response = await client.messages.create(...)
except anthropic.BadRequestError as e:
    # Handle invalid request (400)
except anthropic.AuthenticationError as e:
    # Handle auth failure (401)
except anthropic.RateLimitError as e:
    # Handle rate limit (429)
except anthropic.InternalServerError as e:
    if e.status_code == 529:
        # Handle overload with retry logic

Actual Behavior

All API errors are returned as AssistantMessage objects with:

  1. Error text in message.content[0].text (e.g., "API Error: ...")
  2. Error type in message.error field (e.g., "rate_limit", "invalid_request")
  3. No exception is raised

The error field exists on AssistantMessage but is never checked or used to raise exceptions.

Root Cause Analysis

Based on code investigation, the issue occurs in the message parsing flow:

1. Message Parser (message_parser.py:91-127)

case "assistant":
    try:
        content_blocks: list[ContentBlock] = []
        for block in data["message"]["content"]:
            match block["type"]:
                case "text":
                    content_blocks.append(TextBlock(text=block["text"]))
                    # ❌ No error detection here!

        return AssistantMessage(
            content=content_blocks,
            model=data["message"]["model"],
            parent_tool_use_id=data.get("parent_tool_use_id"),
            error=data["message"].get("error"),  # ✓ Error field is set
        )
    # ❌ But no exception is raised!

2. The Error Field is Never Checked

Searching the entire codebase:

$ grep -r "\.error" --include="*.py" src/
# Only definition found, no usage!
src/claude_agent_sdk/types.py:    error: AssistantMessageError | None = None

No code in the SDK checks the error field on AssistantMessage objects.

3. Flow Diagram

Anthropic API returns error (400/429/529/etc.)
    ↓
Claude CLI receives error and creates assistant message:
    {
      "type": "assistant",
      "message": {
        "content": [{"type": "text", "text": "API Error: ..."}],
        "error": "rate_limit",  # Error type stored here
        "model": "claude-sonnet-4-5"
      }
    }
    ↓
SDK parses message (message_parser.py)
    ↓
Creates AssistantMessage:
    content=[TextBlock("API Error: ...")]
    error="rate_limit"  # Field is set but never checked!
    ↓
Returns to user as normal message (NO EXCEPTION) ❌

Related Issues

This bug manifests in multiple scenarios reported in other issues:

  1. claude-agent-sdk swallows API error messages, returns unhelpful "Unknown error" #437 - SDK swallows API error messages (related to error message clarity)
  2. How to handle API Error: 400 Input is too long for requested model. #431 - "API Error: 400 Input is too long for requested model" - no programmatic way to handle
  3. #8217 (claude-code) - WebFetch 400 model identifier error appears as text
  4. [Bedrock] Subagents default to US region instead of inheriting EU region causing API Error #307 - Bedrock region mismatch errors appear as text

Environment

  • SDK Version: 0.1.18+ (observed in latest)
  • Python Version: 3.12
  • OS: Linux, macOS (production environments)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions