anthropics / anthropics/anthropic-sdk-python
Transport errors while consuming a stream escape as raw httpx exceptions instead of `APITimeoutError` / `APIConnectionError`
- Lingua principale
- Python
- Stelle
- 3.9k
- Fork
- 853
- Merge medio
- 1g 11h
- PR unite (30g)
- 10
Descrizione
`_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
Guida per i contributori
Apri la guida per i contributori
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- python
- Ambito
- api
- Tipo di issue
- Bug
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Stato di attività
- Attiva
- Chiarezza
- Specificata chiaramente
- Idoneità per principianti
- 74/100