anthropics / anthropics/anthropic-sdk-python
Transport errors while consuming a stream escape as raw httpx exceptions instead of `APITimeoutError` / `APIConnectionError`
- Ngôn ngữ chính
- Python
- Star
- 3.9k
- Fork
- 853
- Merge trung bình
- 1 ngày 11 giờ
- Pull request đã merge (30 ngày)
- 10
Mô tả
`_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
Hướng dẫn đóng góp
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- api
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 74/100