Skip to content

Commit dd563c0

Browse files
karpetrosyanstainless-app[bot]
authored andcommitted
feat(client): add error type field to APIStatusError (#1587)
1 parent 8172232 commit dd563c0

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/anthropic/_exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
from __future__ import annotations
44

5+
from typing import Union, cast
56
from typing_extensions import Literal
67

78
import httpx
89

10+
from ._utils import is_dict
11+
from .types.shared.error_type import ErrorType
12+
913
__all__ = [
1014
"BadRequestError",
1115
"AuthenticationError",
@@ -60,13 +64,20 @@ class APIStatusError(APIError):
6064
response: httpx.Response
6165
status_code: int
6266
request_id: str | None
67+
type: ErrorType | None
6368

6469
def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None:
6570
super().__init__(message, response.request, body=body)
6671
self.response = response
6772
self.status_code = response.status_code
6873
self.request_id = response.headers.get("request-id")
6974

75+
self.type = None
76+
if is_dict(body):
77+
error = body.get("error")
78+
if is_dict(error):
79+
self.type = cast(Union[ErrorType, None], error.get("type"))
80+
7081

7182
class APIConnectionError(APIError):
7283
def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None:

tests/test_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,23 @@ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Anthrop
11131113
assert exc_info.value.response.status_code == 302
11141114
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
11151115

1116+
@pytest.mark.respx(base_url=base_url)
1117+
def test_status_error_type_field(self, respx_mock: MockRouter, client: Anthropic) -> None:
1118+
respx_mock.post("/v1/messages").mock(
1119+
return_value=httpx.Response(
1120+
400,
1121+
json={"type": "error", "error": {"type": "invalid_request_error", "message": "Bad request"}},
1122+
)
1123+
)
1124+
with pytest.raises(APIStatusError) as exc_info:
1125+
client.messages.create(
1126+
max_tokens=1024,
1127+
messages=[{"role": "user", "content": "Hello"}],
1128+
model="claude-opus-4-6",
1129+
)
1130+
assert exc_info.value.type == "invalid_request_error"
1131+
assert exc_info.value.status_code == 400
1132+
11161133

11171134
class TestAsyncAnthropic:
11181135
@pytest.mark.respx(base_url=base_url)
@@ -2125,3 +2142,20 @@ async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_cli
21252142

21262143
assert exc_info.value.response.status_code == 302
21272144
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
2145+
2146+
@pytest.mark.respx(base_url=base_url)
2147+
async def test_status_error_type_field(self, respx_mock: MockRouter, async_client: AsyncAnthropic) -> None:
2148+
respx_mock.post("/v1/messages").mock(
2149+
return_value=httpx.Response(
2150+
400,
2151+
json={"type": "error", "error": {"type": "invalid_request_error", "message": "Bad request"}},
2152+
)
2153+
)
2154+
with pytest.raises(APIStatusError) as exc_info:
2155+
await async_client.messages.create(
2156+
max_tokens=1024,
2157+
messages=[{"role": "user", "content": "Hello"}],
2158+
model="claude-opus-4-6",
2159+
)
2160+
assert exc_info.value.type == "invalid_request_error"
2161+
assert exc_info.value.status_code == 400

tests/test_streaming.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import annotations
22

3-
from typing import Iterator, AsyncIterator
3+
from typing import TypeVar, Iterator, AsyncIterator
44

55
import httpx
66
import pytest
77

88
from anthropic import Anthropic, AsyncAnthropic
99
from anthropic._streaming import Stream, AsyncStream, ServerSentEvent
10+
from anthropic._exceptions import APIStatusError
11+
12+
_T = TypeVar("_T")
1013

1114

1215
@pytest.mark.asyncio
@@ -216,6 +219,25 @@ def body() -> Iterator[bytes]:
216219
assert sse.json() == {"content": "известни"}
217220

218221

222+
@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
223+
async def test_error_type(
224+
sync: bool,
225+
client: Anthropic,
226+
async_client: AsyncAnthropic,
227+
) -> None:
228+
def body() -> Iterator[bytes]:
229+
yield b"event: error\n"
230+
yield b'data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}\n\n'
231+
232+
iterator = make_stream_iterator(content=body(), sync=sync, client=client, async_client=async_client)
233+
234+
with pytest.raises(APIStatusError) as exc_info:
235+
await iter_next(iterator)
236+
237+
assert exc_info.value.type == "overloaded_error"
238+
assert "Overloaded" in str(exc_info.value)
239+
240+
219241
def test_isinstance_check(client: Anthropic, async_client: AsyncAnthropic) -> None:
220242
async_stream = AsyncStream(cast_to=object, client=async_client, response=httpx.Response(200, content=b"foo"))
221243
assert isinstance(async_stream, AsyncStream)
@@ -229,7 +251,7 @@ async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]:
229251
yield chunk
230252

231253

232-
async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent:
254+
async def iter_next(iter: Iterator[_T] | AsyncIterator[_T]) -> _T:
233255
if isinstance(iter, AsyncIterator):
234256
return await iter.__anext__()
235257

@@ -254,3 +276,27 @@ def make_event_iterator(
254276
return AsyncStream(
255277
cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content))
256278
)._iter_events()
279+
280+
281+
# Unlike make_event_iterator which only parses SSE events using _iter_events(),
282+
# this helper uses __stream__() to process the full stream pipeline including
283+
# parsing message objects and converting error events into raised exceptions.
284+
def make_stream_iterator(
285+
content: Iterator[bytes],
286+
*,
287+
sync: bool,
288+
client: Anthropic,
289+
async_client: AsyncAnthropic,
290+
) -> AsyncIterator[object] | Iterator[object]:
291+
if sync:
292+
return Stream(
293+
cast_to=object,
294+
client=client,
295+
response=httpx.Response(200, content=content, request=httpx.Request("GET", "https://example.com")),
296+
).__stream__()
297+
298+
return AsyncStream(
299+
cast_to=object,
300+
client=async_client,
301+
response=httpx.Response(200, content=to_aiter(content), request=httpx.Request("GET", "https://example.com")),
302+
).__stream__()

0 commit comments

Comments
 (0)