Skip to content

Commit 6cb170c

Browse files
Require concrete int HTTP status codes in transport responses
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent a2be91f commit 6cb170c

File tree

5 files changed

+62
-3
lines changed

5 files changed

+62
-3
lines changed

hyperbrowser/transport/async_transport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
3434
def _normalize_response_status_code(self, response: httpx.Response) -> int:
3535
try:
3636
status_code = response.status_code
37-
if isinstance(status_code, bool) or not isinstance(status_code, int):
37+
if type(status_code) is not int:
3838
raise TypeError("status code must be an integer")
3939
normalized_status_code = status_code
4040
if not (

hyperbrowser/transport/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class APIResponse(Generic[T]):
116116
"""
117117

118118
def __init__(self, data: Optional[Union[dict, T]] = None, status_code: int = 200):
119-
if isinstance(status_code, bool) or not isinstance(status_code, int):
119+
if type(status_code) is not int:
120120
raise HyperbrowserError("status_code must be an integer")
121121
if not (_MIN_HTTP_STATUS_CODE <= status_code <= _MAX_HTTP_STATUS_CODE):
122122
raise HyperbrowserError("status_code must be between 100 and 599")

hyperbrowser/transport/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, api_key: str, headers: Optional[Mapping[str, str]] = None):
3333
def _normalize_response_status_code(self, response: httpx.Response) -> int:
3434
try:
3535
status_code = response.status_code
36-
if isinstance(status_code, bool) or not isinstance(status_code, int):
36+
if type(status_code) is not int:
3737
raise TypeError("status code must be an integer")
3838
normalized_status_code = status_code
3939
if not (

tests/test_transport_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ def __getitem__(self, key: str) -> object:
145145
raise KeyError(key)
146146

147147

148+
class _StatusCodeIntSubclass(int):
149+
pass
150+
151+
148152
def test_api_response_from_json_parses_model_data() -> None:
149153
response = APIResponse.from_json(
150154
{"name": "job-1", "retries": 2}, _SampleResponseModel
@@ -345,6 +349,11 @@ def test_api_response_constructor_rejects_boolean_status_code() -> None:
345349
APIResponse(status_code=True)
346350

347351

352+
def test_api_response_constructor_rejects_integer_subclass_status_code() -> None:
353+
with pytest.raises(HyperbrowserError, match="status_code must be an integer"):
354+
APIResponse(status_code=_StatusCodeIntSubclass(200))
355+
356+
348357
@pytest.mark.parametrize("status_code", [99, 600])
349358
def test_api_response_constructor_rejects_out_of_range_status_code(
350359
status_code: int,
@@ -360,6 +369,11 @@ def test_api_response_from_status_rejects_boolean_status_code() -> None:
360369
APIResponse.from_status(True) # type: ignore[arg-type]
361370

362371

372+
def test_api_response_from_status_rejects_integer_subclass_status_code() -> None:
373+
with pytest.raises(HyperbrowserError, match="status_code must be an integer"):
374+
APIResponse.from_status(_StatusCodeIntSubclass(200))
375+
376+
363377
@pytest.mark.parametrize("status_code", [99, 600])
364378
def test_api_response_from_status_rejects_out_of_range_status_code(
365379
status_code: int,

tests/test_transport_response_handling.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ def json(self):
106106
return {}
107107

108108

109+
class _IntegerSubclassStatusNoContentResponse:
110+
content = b""
111+
text = ""
112+
113+
class _StatusCode(int):
114+
pass
115+
116+
status_code = _StatusCode(200)
117+
118+
def raise_for_status(self) -> None:
119+
return None
120+
121+
def json(self):
122+
return {}
123+
124+
109125
class _BrokenStatusCodeHttpErrorResponse:
110126
content = b""
111127
text = "status error"
@@ -231,6 +247,19 @@ def test_sync_handle_response_with_non_integer_status_raises_hyperbrowser_error(
231247
transport.close()
232248

233249

250+
def test_sync_handle_response_with_integer_subclass_status_raises_hyperbrowser_error():
251+
transport = SyncTransport(api_key="test-key")
252+
try:
253+
with pytest.raises(
254+
HyperbrowserError, match="Failed to process response status code"
255+
):
256+
transport._handle_response(
257+
_IntegerSubclassStatusNoContentResponse() # type: ignore[arg-type]
258+
)
259+
finally:
260+
transport.close()
261+
262+
234263
def test_sync_handle_response_with_request_error_includes_method_and_url():
235264
transport = SyncTransport(api_key="test-key")
236265
try:
@@ -420,6 +449,22 @@ async def run() -> None:
420449
asyncio.run(run())
421450

422451

452+
def test_async_handle_response_with_integer_subclass_status_raises_hyperbrowser_error():
453+
async def run() -> None:
454+
transport = AsyncTransport(api_key="test-key")
455+
try:
456+
with pytest.raises(
457+
HyperbrowserError, match="Failed to process response status code"
458+
):
459+
await transport._handle_response(
460+
_IntegerSubclassStatusNoContentResponse() # type: ignore[arg-type]
461+
)
462+
finally:
463+
await transport.close()
464+
465+
asyncio.run(run())
466+
467+
423468
def test_async_handle_response_with_request_error_includes_method_and_url():
424469
async def run() -> None:
425470
transport = AsyncTransport(api_key="test-key")

0 commit comments

Comments
 (0)