anthropics / anthropics/anthropic-sdk-python

Transport errors while consuming a stream escape as raw httpx exceptions instead of `APITimeoutError` / `APIConnectionError`

Offen
#1,919 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Python
Sterne
3.9k
Forks
853
Ø Merge
1 T. 11 Std.
Gemergte PRs (30 T.)
10

Beschreibung

`_base_client` wraps the initial send: `httpx.TimeoutException` becomes `APITimeoutError`, other transport errors become `APIConnectionError`, and both go through the retry loop (`_base_client.py` around lines 1291-1300 on 1.4.0). Once the response is streaming, `_streaming.py` iterates the response with no handling at all, so a read timeout or a dropped connection mid-stream surfaces as the raw `httpx.ReadTimeout` / `httpx.RemoteProtocolError` (`httpx2.*` on 1.x). That is not an `APIError`, and `max_retries` is not consulted.

So `except anthropic.APIError` around a streaming call misses the most common streaming failure, and a client with `max_retries=2` makes exactly one request.

Offline repro, no network. The mock transport yields one event and then raises `ReadTimeout`:

```python
import platform
import sys

import anthropic

try:
import httpx2 as httpx # anthropic >= 1.0
except ImportError:
import httpx

FIRST = b'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"claude-sonnet-4-6","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":0}}}\n\n'
requests_seen = 0

class DiesMidStream(httpx.SyncByteStream):
def __iter__(self):
yield FIRST
raise httpx.ReadTimeout("timed out while reading the stream")

def handler(request: httpx.Request) -> httpx.Response:
global requests_seen
requests_seen += 1
return httpx.Response(200, headers={"content-type": "text/event-stream"}, stream=DiesMidStream())

client = anthropic.Anthropic(
api_key="x", http_client=httpx.Client(transport=httpx.MockTransport(handler)), max_retries=2
)
print(f"python {sys.version.split()[0]} {platform.system()}, anthropic {anthropic.__version__}, httpx {httpx.__version__}")
try:
with client.messages.stream(model="claude-sonnet-4-6", max_tokens=8, messages=[{"role": "user", "content": "hi"}]) as s:
for _ in s:
pass
except Exception as e: # noqa: BLE001
print(f"escaped: {type(e).__module__}.{type(e).__name__}: {e}")
print(f"isinstance(e, anthropic.APIError) = {isinstance(e, anthropic.APIError)}")
print(f"requests made with max_retries=2: {requests_seen}")
```

Output (python 3.12.13, macOS):

```
anthropic 0.122.0, httpx 0.28.1
escaped: httpx.ReadTimeout: timed out while reading the stream
isinstance(e, anthropic.APIError) = False
requests made with max_retries=2: 1

anthropic 1.4.0, httpx 2.12.0
escaped: httpx2.ReadTimeout: timed out while reading the stream
isinstance(e, anthropic.APIError) = False
requests made with max_retries=2: 1
```

Expected: the same wrapping as the initial send, `APITimeoutError` / `APIConnectionError`, so the exception hierarchy holds for the whole call. Whether a partially consumed stream can be retried is a separate question, since the caller has already seen events; wrapping alone fixes the catch side. openai-python has the same gap, same generated base: https://github.com/openai/openai-python/issues/3811

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Read _base_client.py around lines 1291-1300 to compare initial-send exception handling with _streaming.py, then reproduce the offline mock transport case from the issue. The work is done when mid-stream timeout and connection failures are exposed as the documented API exception types while preserving the existing streaming behavior.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
api
Issue-Typ
Bug
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
74/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.