Skip to content

Wrap mid-stream httpx.TransportError as APIConnectionError#1550

Draft
mengtingli-ant wants to merge 1 commit into
mainfrom
mengtingli/wrap-mid-stream-transport-errors
Draft

Wrap mid-stream httpx.TransportError as APIConnectionError#1550
mengtingli-ant wants to merge 1 commit into
mainfrom
mengtingli/wrap-mid-stream-transport-errors

Conversation

@mengtingli-ant
Copy link
Copy Markdown

What

Wrap mid-stream httpx.TransportError (during SSE body iteration) as anthropic.APIConnectionError, in both Stream._iter_events and AsyncStream._iter_events.

Why

Mid-stream transport drops (RemoteProtocolError, ReadError, ConnectError) currently leak through as bare httpx exceptions because the SDK's wrapping in _base_client._request only covers the pre-body request. Once the SSE 200 is sent and body iteration starts, there's no try/except in _iter_events.

This means customers' standard retry ladders:

except anthropic.APIConnectionError:
    retry()

…miss mid-stream drops. They have to know to also catch httpx.TransportError, which nobody discovers without debugging.

Found while reproducing DeepSearchQA via the public API: 35/45 terminal question failures were RemoteProtocolError("peer closed connection without sending complete message body") that the standard retry ladder missed. Context: Slack thread.

Changes

  • Stream._iter_events / AsyncStream._iter_events: wrap httpx.TransportError as APIConnectionError (same pattern as _base_client.py:1104); let TimeoutException pass through unchanged so it doesn't get double-wrapped (APITimeoutError is already an APIConnectionError subclass).
  • 2 tests: mid-stream RemoteProtocolError is wrapped (with __cause__ preserved); mid-stream ReadTimeout passes through.

Not in this PR

The follow-up — auto-retry the full request inside MessageStream.get_final_message() on mid-stream APIConnectionError — is a larger behavior change. This PR just makes the exception catchable with the right type.

🤖 Generated with Claude Code

Mid-stream transport drops during SSE iteration (RemoteProtocolError,
ReadError, ConnectError, …) leak through as bare httpx exceptions because
the SDK's wrapping in _base_client._request only covers the pre-body
request — once the SSE 200 is sent and body iteration starts, _iter_events
has no try/except. Customers' standard `except anthropic.APIConnectionError:`
retry ladders therefore miss mid-stream drops and have to know to also catch
`httpx.TransportError`.

This wraps mid-stream TransportError (in both Stream and AsyncStream) as
APIConnectionError with the original preserved as __cause__, matching the
pattern at _base_client.py:1104. TimeoutException (a TransportError subclass)
passes through unchanged so it doesn't get double-wrapped — APITimeoutError
is already an APIConnectionError subclass.

Found while reproducing DeepSearchQA via the public API: 35/45 terminal
question failures were RemoteProtocolError that the standard retry ladder
missed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant