Skip to content

Commit c445731

Browse files
Enforce HTTP status range in transport responses
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 0889737 commit c445731

3 files changed

Lines changed: 71 additions & 2 deletions

File tree

hyperbrowser/transport/async_transport.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
class AsyncTransport(AsyncTransportStrategy):
1616
"""Asynchronous transport implementation using httpx"""
1717

18+
_MIN_HTTP_STATUS_CODE = 100
19+
_MAX_HTTP_STATUS_CODE = 599
20+
1821
def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
1922
if not isinstance(api_key, str):
2023
raise HyperbrowserError("api_key must be a string")
@@ -42,7 +45,14 @@ def _normalize_response_status_code(self, response: httpx.Response) -> int:
4245
status_code = response.status_code
4346
if isinstance(status_code, bool):
4447
raise TypeError("boolean status code is invalid")
45-
return int(status_code)
48+
normalized_status_code = int(status_code)
49+
if not (
50+
self._MIN_HTTP_STATUS_CODE
51+
<= normalized_status_code
52+
<= self._MAX_HTTP_STATUS_CODE
53+
):
54+
raise ValueError("status code is outside HTTP range")
55+
return normalized_status_code
4656
except HyperbrowserError:
4757
raise
4858
except Exception as exc:

hyperbrowser/transport/sync.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
class SyncTransport(SyncTransportStrategy):
1616
"""Synchronous transport implementation using httpx"""
1717

18+
_MIN_HTTP_STATUS_CODE = 100
19+
_MAX_HTTP_STATUS_CODE = 599
20+
1821
def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
1922
if not isinstance(api_key, str):
2023
raise HyperbrowserError("api_key must be a string")
@@ -41,7 +44,14 @@ def _normalize_response_status_code(self, response: httpx.Response) -> int:
4144
status_code = response.status_code
4245
if isinstance(status_code, bool):
4346
raise TypeError("boolean status code is invalid")
44-
return int(status_code)
47+
normalized_status_code = int(status_code)
48+
if not (
49+
self._MIN_HTTP_STATUS_CODE
50+
<= normalized_status_code
51+
<= self._MAX_HTTP_STATUS_CODE
52+
):
53+
raise ValueError("status code is outside HTTP range")
54+
return normalized_status_code
4555
except HyperbrowserError:
4656
raise
4757
except Exception as exc:

tests/test_transport_response_handling.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ def json(self):
7878
return {}
7979

8080

81+
class _OutOfRangeStatusNoContentResponse:
82+
content = b""
83+
text = ""
84+
85+
def __init__(self, status_code: int) -> None:
86+
self.status_code = status_code
87+
88+
def raise_for_status(self) -> None:
89+
return None
90+
91+
def json(self):
92+
return {}
93+
94+
8195
class _BrokenStatusCodeHttpErrorResponse:
8296
content = b""
8397
text = "status error"
@@ -171,6 +185,22 @@ def test_sync_handle_response_with_http_status_error_and_broken_status_code():
171185
transport.close()
172186

173187

188+
@pytest.mark.parametrize("status_code", [99, 600])
189+
def test_sync_handle_response_with_out_of_range_status_raises_hyperbrowser_error(
190+
status_code: int,
191+
):
192+
transport = SyncTransport(api_key="test-key")
193+
try:
194+
with pytest.raises(
195+
HyperbrowserError, match="Failed to process response status code"
196+
):
197+
transport._handle_response(
198+
_OutOfRangeStatusNoContentResponse(status_code) # type: ignore[arg-type]
199+
)
200+
finally:
201+
transport.close()
202+
203+
174204
def test_sync_handle_response_with_request_error_includes_method_and_url():
175205
transport = SyncTransport(api_key="test-key")
176206
try:
@@ -322,6 +352,25 @@ async def run() -> None:
322352
asyncio.run(run())
323353

324354

355+
@pytest.mark.parametrize("status_code", [99, 600])
356+
def test_async_handle_response_with_out_of_range_status_raises_hyperbrowser_error(
357+
status_code: int,
358+
):
359+
async def run() -> None:
360+
transport = AsyncTransport(api_key="test-key")
361+
try:
362+
with pytest.raises(
363+
HyperbrowserError, match="Failed to process response status code"
364+
):
365+
await transport._handle_response(
366+
_OutOfRangeStatusNoContentResponse(status_code) # type: ignore[arg-type]
367+
)
368+
finally:
369+
await transport.close()
370+
371+
asyncio.run(run())
372+
373+
325374
def test_async_handle_response_with_request_error_includes_method_and_url():
326375
async def run() -> None:
327376
transport = AsyncTransport(api_key="test-key")

0 commit comments

Comments
 (0)