Skip to content

Commit b35bd34

Browse files
Normalize transport status code handling across response paths
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 6a36209 commit b35bd34

3 files changed

Lines changed: 122 additions & 24 deletions

File tree

hyperbrowser/transport/async_transport.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
3737
self.client = httpx.AsyncClient(headers=merged_headers)
3838
self._closed = False
3939

40+
def _normalize_response_status_code(self, response: httpx.Response) -> int:
41+
try:
42+
status_code = response.status_code
43+
if isinstance(status_code, bool):
44+
raise TypeError("boolean status code is invalid")
45+
return int(status_code)
46+
except HyperbrowserError:
47+
raise
48+
except Exception as exc:
49+
raise HyperbrowserError(
50+
"Failed to process response status code",
51+
original_error=exc,
52+
) from exc
53+
4054
async def close(self) -> None:
4155
if not self._closed:
4256
await self.client.aclose()
@@ -51,21 +65,12 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
5165
async def _handle_response(self, response: httpx.Response) -> APIResponse:
5266
try:
5367
response.raise_for_status()
68+
normalized_status_code = self._normalize_response_status_code(response)
5469
try:
5570
if not response.content:
56-
return APIResponse.from_status(response.status_code)
71+
return APIResponse.from_status(normalized_status_code)
5772
return APIResponse(response.json())
5873
except Exception as e:
59-
try:
60-
status_code = response.status_code
61-
if isinstance(status_code, bool):
62-
raise TypeError("boolean status code is invalid")
63-
normalized_status_code = int(status_code)
64-
except Exception as status_exc:
65-
raise HyperbrowserError(
66-
"Failed to process response status code",
67-
original_error=status_exc,
68-
) from status_exc
6974
if normalized_status_code >= 400:
7075
try:
7176
response_text = response.text
@@ -80,9 +85,10 @@ async def _handle_response(self, response: httpx.Response) -> APIResponse:
8085
return APIResponse.from_status(normalized_status_code)
8186
except httpx.HTTPStatusError as e:
8287
message = extract_error_message(response, fallback_error=e)
88+
normalized_status_code = self._normalize_response_status_code(response)
8389
raise HyperbrowserError(
8490
message,
85-
status_code=response.status_code,
91+
status_code=normalized_status_code,
8692
response=response,
8793
original_error=e,
8894
)

hyperbrowser/transport/sync.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,29 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
3636
)
3737
self.client = httpx.Client(headers=merged_headers)
3838

39+
def _normalize_response_status_code(self, response: httpx.Response) -> int:
40+
try:
41+
status_code = response.status_code
42+
if isinstance(status_code, bool):
43+
raise TypeError("boolean status code is invalid")
44+
return int(status_code)
45+
except HyperbrowserError:
46+
raise
47+
except Exception as exc:
48+
raise HyperbrowserError(
49+
"Failed to process response status code",
50+
original_error=exc,
51+
) from exc
52+
3953
def _handle_response(self, response: httpx.Response) -> APIResponse:
4054
try:
4155
response.raise_for_status()
56+
normalized_status_code = self._normalize_response_status_code(response)
4257
try:
4358
if not response.content:
44-
return APIResponse.from_status(response.status_code)
59+
return APIResponse.from_status(normalized_status_code)
4560
return APIResponse(response.json())
4661
except Exception as e:
47-
try:
48-
status_code = response.status_code
49-
if isinstance(status_code, bool):
50-
raise TypeError("boolean status code is invalid")
51-
normalized_status_code = int(status_code)
52-
except Exception as status_exc:
53-
raise HyperbrowserError(
54-
"Failed to process response status code",
55-
original_error=status_exc,
56-
) from status_exc
5762
if normalized_status_code >= 400:
5863
try:
5964
response_text = response.text
@@ -68,9 +73,10 @@ def _handle_response(self, response: httpx.Response) -> APIResponse:
6873
return APIResponse.from_status(normalized_status_code)
6974
except httpx.HTTPStatusError as e:
7075
message = extract_error_message(response, fallback_error=e)
76+
normalized_status_code = self._normalize_response_status_code(response)
7177
raise HyperbrowserError(
7278
message,
73-
status_code=response.status_code,
79+
status_code=normalized_status_code,
7480
response=response,
7581
original_error=e,
7682
)

tests/test_transport_response_handling.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,34 @@ def json(self):
6666
raise RuntimeError("broken json")
6767

6868

69+
class _BooleanStatusNoContentResponse:
70+
status_code = True
71+
content = b""
72+
text = ""
73+
74+
def raise_for_status(self) -> None:
75+
return None
76+
77+
def json(self):
78+
return {}
79+
80+
81+
class _BrokenStatusCodeHttpErrorResponse:
82+
content = b""
83+
text = "status error"
84+
85+
def raise_for_status(self) -> None:
86+
request = httpx.Request("GET", "https://example.com/status-error")
87+
raise httpx.HTTPStatusError("status failure", request=request, response=self)
88+
89+
@property
90+
def status_code(self) -> int:
91+
raise RuntimeError("broken status code")
92+
93+
def json(self):
94+
return {"message": "status failure"}
95+
96+
6997
def test_sync_handle_response_with_non_json_success_body_returns_status_only():
7098
transport = SyncTransport(api_key="test-key")
7199
try:
@@ -117,6 +145,32 @@ def test_sync_handle_response_with_broken_status_code_raises_hyperbrowser_error(
117145
transport.close()
118146

119147

148+
def test_sync_handle_response_with_boolean_status_no_content_raises_hyperbrowser_error():
149+
transport = SyncTransport(api_key="test-key")
150+
try:
151+
with pytest.raises(
152+
HyperbrowserError, match="Failed to process response status code"
153+
):
154+
transport._handle_response(
155+
_BooleanStatusNoContentResponse() # type: ignore[arg-type]
156+
)
157+
finally:
158+
transport.close()
159+
160+
161+
def test_sync_handle_response_with_http_status_error_and_broken_status_code():
162+
transport = SyncTransport(api_key="test-key")
163+
try:
164+
with pytest.raises(
165+
HyperbrowserError, match="Failed to process response status code"
166+
):
167+
transport._handle_response(
168+
_BrokenStatusCodeHttpErrorResponse() # type: ignore[arg-type]
169+
)
170+
finally:
171+
transport.close()
172+
173+
120174
def test_sync_handle_response_with_request_error_includes_method_and_url():
121175
transport = SyncTransport(api_key="test-key")
122176
try:
@@ -236,6 +290,38 @@ async def run() -> None:
236290
asyncio.run(run())
237291

238292

293+
def test_async_handle_response_with_boolean_status_no_content_raises_hyperbrowser_error():
294+
async def run() -> None:
295+
transport = AsyncTransport(api_key="test-key")
296+
try:
297+
with pytest.raises(
298+
HyperbrowserError, match="Failed to process response status code"
299+
):
300+
await transport._handle_response(
301+
_BooleanStatusNoContentResponse() # type: ignore[arg-type]
302+
)
303+
finally:
304+
await transport.close()
305+
306+
asyncio.run(run())
307+
308+
309+
def test_async_handle_response_with_http_status_error_and_broken_status_code():
310+
async def run() -> None:
311+
transport = AsyncTransport(api_key="test-key")
312+
try:
313+
with pytest.raises(
314+
HyperbrowserError, match="Failed to process response status code"
315+
):
316+
await transport._handle_response(
317+
_BrokenStatusCodeHttpErrorResponse() # type: ignore[arg-type]
318+
)
319+
finally:
320+
await transport.close()
321+
322+
asyncio.run(run())
323+
324+
239325
def test_async_handle_response_with_request_error_includes_method_and_url():
240326
async def run() -> None:
241327
transport = AsyncTransport(api_key="test-key")

0 commit comments

Comments
 (0)