Skip to content

Mid-stream SSE errors get status_code=200 instead of the actual error code #1258

Description

@sarth6

Bug

When the API returns HTTP 200 (streaming started fine) but then sends an SSE error event like overloaded_error, the SDK creates an APIStatusError with status_code=200 instead of 529. The error body has the right type, but the status code is wrong.

This happens because _streaming.py passes self.response (the original HTTP response object) to _make_status_error, and _make_status_error switches on response.status_code to pick the error subclass.

Code path

In _streaming.py (line ~229):

if sse.event == "error":
    body = sse.json()
    err_msg = f"{body}"
    raise self._client._make_status_error(
        err_msg,
        body=body,
        response=self.response,  # <-- this is the original HTTP 200 response
    )

In _client.py, _make_status_error dispatches on response.status_code:

def _make_status_error(self, err_msg, *, body, response):
    if response.status_code == 429:
        return RateLimitError(...)
    if response.status_code >= 500:
        return InternalServerError(...)
    return APIStatusError(...)  # <-- falls through here because status is 200

Since the HTTP status was 200, none of the specific branches match. You get a bare APIStatusError with status_code=200.

Why this matters

Any caller checking status_code for retry/fallback decisions gets the wrong answer. An overloaded_error should look like a 529, not a 200. The body has the correct error type ({'error': {'type': 'overloaded_error'}}), but most callers don't expect to need to parse that — they check status codes.

We hit this in production with pydantic-ai's FallbackModel, which checks status_code >= 500 to decide whether to try the next model. The overloaded error had status_code=200, so fallback never fired.

Expected behavior

Mid-stream SSE errors should produce the same error subclass as if the error came back as an HTTP response. An overloaded_error → OverloadedError(status_code=529), an api_error → InternalServerError(status_code=500), etc.

Suggested fix

Map the SSE error type to the corresponding status code before calling _make_status_error, or construct the right error subclass directly:

_SSE_ERROR_TYPE_TO_STATUS = {
    "overloaded_error": 529,
    "rate_limit_error": 429,
    "api_error": 500,
    "internal_server_error": 500,
    # client errors (shouldn't appear mid-stream, but for completeness):
    "authentication_error": 401,
    "invalid_request_error": 400,
    "not_found_error": 404,
}

Then use the mapped status code (falling back to the original response.status_code) when building the error.

Versions

  • anthropic SDK: 0.52.0
  • Observed with claude-sonnet-4-20250514 via the streaming API

Activity

  1. weiguangli-io commented on Mar 19, 2026

    @weiguangli-io

    Confirmed the bug by reading the current source. Here's a concise breakdown:

    Root cause: In _streaming.py (lines 114-117 for sync, 234-237 for async), the SSE error handler passes self.response — the original HTTP 200 response — to _client._make_status_error(). Since _make_status_error in _client.py dispatches entirely on response.status_code, every mid-stream SSE error falls through to the generic APIStatusError with status_code=200, regardless of the error type in the body.

    The gap: SSE error events carry the error type (e.g. overloaded_error) in the JSON body but no HTTP status code. There is currently no translation layer between these two representations.

    Fix: Add a mapping from SSE error types to their canonical HTTP status codes in _streaming.py, and use it to construct the correct error subclass. Something like:

    _SSE_ERROR_TO_STATUS = {
        "overloaded_error": 529,
        "rate_limit_error": 429,
        "api_error": 500,
        "authentication_error": 401,
        "invalid_request_error": 400,
        "not_found_error": 404,
    }

    Then in the sse.event == "error" block, extract the error type from the parsed body, look up the status code, and either (a) build a synthetic response with the mapped status code before calling _make_status_error, or (b) construct the right error subclass directly, bypassing _make_status_error for SSE errors.

    Option (b) is cleaner since it avoids mutating or faking the httpx response object.

    Happy to submit a PR for this if it would be welcome.

  2. added a commit that references this issue on Mar 19, 2026
    0b47292
  3. dtmeadows commented on Mar 19, 2026

    @dtmeadows
    Contributor

    Thanks @sarth6 for the detailed report! We're writing out a better approach to this that we hope to have ready for release shortly! I'll update this issue once that goes out.

  4. added a commit that references this issue on Mar 20, 2026
    449c520
  5. linked a pull request that will close this issuerelease: 0.87.0 #1264on Mar 23, 2026
  6. khalidsaidi commented on Mar 24, 2026

    @khalidsaidi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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