Skip to content

Commit a761e17

Browse files
authored
is_informational / is_success / is_redirect / is_client_error / is_server_error (#1854)
1 parent ff9813e commit a761e17

File tree

4 files changed

+157
-32
lines changed

4 files changed

+157
-32
lines changed

httpx/_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ def _send_handling_redirects(
945945
hook(response)
946946
response.history = list(history)
947947

948-
if not response.is_redirect:
948+
if not response.has_redirect_location:
949949
return response
950950

951951
request = self._build_redirect_request(request, response)
@@ -1640,7 +1640,7 @@ async def _send_handling_redirects(
16401640

16411641
response.history = list(history)
16421642

1643-
if not response.is_redirect:
1643+
if not response.has_redirect_location:
16441644
return response
16451645

16461646
request = self._build_redirect_request(request, response)

httpx/_models.py

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,36 +1399,112 @@ def _get_content_decoder(self) -> ContentDecoder:
13991399

14001400
return self._decoder
14011401

1402+
@property
1403+
def is_informational(self) -> bool:
1404+
"""
1405+
A property which is `True` for 1xx status codes, `False` otherwise.
1406+
"""
1407+
return codes.is_informational(self.status_code)
1408+
1409+
@property
1410+
def is_success(self) -> bool:
1411+
"""
1412+
A property which is `True` for 2xx status codes, `False` otherwise.
1413+
"""
1414+
return codes.is_success(self.status_code)
1415+
1416+
@property
1417+
def is_redirect(self) -> bool:
1418+
"""
1419+
A property which is `True` for 3xx status codes, `False` otherwise.
1420+
1421+
Note that not all responses with a 3xx status code indicate a URL redirect.
1422+
1423+
Use `response.has_redirect_location` to determine responses with a properly
1424+
formed URL redirection.
1425+
"""
1426+
return codes.is_redirect(self.status_code)
1427+
1428+
@property
1429+
def is_client_error(self) -> bool:
1430+
"""
1431+
A property which is `True` for 4xx status codes, `False` otherwise.
1432+
"""
1433+
return codes.is_client_error(self.status_code)
1434+
1435+
@property
1436+
def is_server_error(self) -> bool:
1437+
"""
1438+
A property which is `True` for 5xx status codes, `False` otherwise.
1439+
"""
1440+
return codes.is_server_error(self.status_code)
1441+
14021442
@property
14031443
def is_error(self) -> bool:
1444+
"""
1445+
A property which is `True` for 4xx and 5xx status codes, `False` otherwise.
1446+
"""
14041447
return codes.is_error(self.status_code)
14051448

14061449
@property
1407-
def is_redirect(self) -> bool:
1408-
return codes.is_redirect(self.status_code) and "location" in self.headers
1450+
def has_redirect_location(self) -> bool:
1451+
"""
1452+
Returns True for 3xx responses with a properly formed URL redirection,
1453+
`False` otherwise.
1454+
"""
1455+
return (
1456+
self.status_code
1457+
in (
1458+
# 301 (Cacheable redirect. Method may change to GET.)
1459+
codes.MOVED_PERMANENTLY,
1460+
# 302 (Uncacheable redirect. Method may change to GET.)
1461+
codes.FOUND,
1462+
# 303 (Client should make a GET or HEAD request.)
1463+
codes.SEE_OTHER,
1464+
# 307 (Equiv. 302, but retain method)
1465+
codes.TEMPORARY_REDIRECT,
1466+
# 308 (Equiv. 301, but retain method)
1467+
codes.PERMANENT_REDIRECT,
1468+
)
1469+
and "Location" in self.headers
1470+
)
14091471

14101472
def raise_for_status(self) -> None:
14111473
"""
14121474
Raise the `HTTPStatusError` if one occurred.
14131475
"""
1414-
message = (
1415-
"{0.status_code} {error_type}: {0.reason_phrase} for url: {0.url}\n"
1416-
"For more information check: https://httpstatuses.com/{0.status_code}"
1417-
)
1418-
14191476
request = self._request
14201477
if request is None:
14211478
raise RuntimeError(
14221479
"Cannot call `raise_for_status` as the request "
14231480
"instance has not been set on this response."
14241481
)
14251482

1426-
if codes.is_client_error(self.status_code):
1427-
message = message.format(self, error_type="Client Error")
1428-
raise HTTPStatusError(message, request=request, response=self)
1429-
elif codes.is_server_error(self.status_code):
1430-
message = message.format(self, error_type="Server Error")
1431-
raise HTTPStatusError(message, request=request, response=self)
1483+
if self.is_success:
1484+
return
1485+
1486+
if self.has_redirect_location:
1487+
message = (
1488+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
1489+
"Redirect location: '{0.headers[location]}'\n"
1490+
"For more information check: https://httpstatuses.com/{0.status_code}"
1491+
)
1492+
else:
1493+
message = (
1494+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
1495+
"For more information check: https://httpstatuses.com/{0.status_code}"
1496+
)
1497+
1498+
status_class = self.status_code // 100
1499+
error_types = {
1500+
1: "Informational response",
1501+
3: "Redirect response",
1502+
4: "Client error",
1503+
5: "Server error",
1504+
}
1505+
error_type = error_types.get(status_class, "Invalid status code")
1506+
message = message.format(self, error_type=error_type)
1507+
raise HTTPStatusError(message, request=request, response=self)
14321508

14331509
def json(self, **kwargs: typing.Any) -> typing.Any:
14341510
if self.charset_encoding is None and self.content and len(self.content) > 3:

httpx/_status_codes.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,47 @@ def get_reason_phrase(cls, value: int) -> str:
3939
return ""
4040

4141
@classmethod
42-
def is_redirect(cls, value: int) -> bool:
43-
return value in (
44-
# 301 (Cacheable redirect. Method may change to GET.)
45-
codes.MOVED_PERMANENTLY,
46-
# 302 (Uncacheable redirect. Method may change to GET.)
47-
codes.FOUND,
48-
# 303 (Client should make a GET or HEAD request.)
49-
codes.SEE_OTHER,
50-
# 307 (Equiv. 302, but retain method)
51-
codes.TEMPORARY_REDIRECT,
52-
# 308 (Equiv. 301, but retain method)
53-
codes.PERMANENT_REDIRECT,
54-
)
42+
def is_informational(cls, value: int) -> bool:
43+
"""
44+
Returns `True` for 1xx status codes, `False` otherwise.
45+
"""
46+
return 100 <= value <= 199
5547

5648
@classmethod
57-
def is_error(cls, value: int) -> bool:
58-
return 400 <= value <= 599
49+
def is_success(cls, value: int) -> bool:
50+
"""
51+
Returns `True` for 2xx status codes, `False` otherwise.
52+
"""
53+
return 200 <= value <= 299
54+
55+
@classmethod
56+
def is_redirect(cls, value: int) -> bool:
57+
"""
58+
Returns `True` for 3xx status codes, `False` otherwise.
59+
"""
60+
return 300 <= value <= 399
5961

6062
@classmethod
6163
def is_client_error(cls, value: int) -> bool:
64+
"""
65+
Returns `True` for 4xx status codes, `False` otherwise.
66+
"""
6267
return 400 <= value <= 499
6368

6469
@classmethod
6570
def is_server_error(cls, value: int) -> bool:
71+
"""
72+
Returns `True` for 5xx status codes, `False` otherwise.
73+
"""
6674
return 500 <= value <= 599
6775

76+
@classmethod
77+
def is_error(cls, value: int) -> bool:
78+
"""
79+
Returns `True` for 4xx or 5xx status codes, `False` otherwise.
80+
"""
81+
return 400 <= value <= 599
82+
6883
# informational
6984
CONTINUE = 100, "Continue"
7085
SWITCHING_PROTOCOLS = 101, "Switching Protocols"

tests/models/test_responses.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,49 @@ def test_raise_for_status():
9090
response = httpx.Response(200, request=request)
9191
response.raise_for_status()
9292

93+
# 1xx status codes are informational responses.
94+
response = httpx.Response(101, request=request)
95+
assert response.is_informational
96+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
97+
response.raise_for_status()
98+
assert str(exc_info.value) == (
99+
"Informational response '101 Switching Protocols' for url 'https://example.org'\n"
100+
"For more information check: https://httpstatuses.com/101"
101+
)
102+
103+
# 3xx status codes are redirections.
104+
headers = {"location": "https://other.org"}
105+
response = httpx.Response(303, headers=headers, request=request)
106+
assert response.is_redirect
107+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
108+
response.raise_for_status()
109+
assert str(exc_info.value) == (
110+
"Redirect response '303 See Other' for url 'https://example.org'\n"
111+
"Redirect location: 'https://other.org'\n"
112+
"For more information check: https://httpstatuses.com/303"
113+
)
114+
93115
# 4xx status codes are a client error.
94116
response = httpx.Response(403, request=request)
95-
with pytest.raises(httpx.HTTPStatusError):
117+
assert response.is_client_error
118+
assert response.is_error
119+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
96120
response.raise_for_status()
121+
assert str(exc_info.value) == (
122+
"Client error '403 Forbidden' for url 'https://example.org'\n"
123+
"For more information check: https://httpstatuses.com/403"
124+
)
97125

98126
# 5xx status codes are a server error.
99127
response = httpx.Response(500, request=request)
100-
with pytest.raises(httpx.HTTPStatusError):
128+
assert response.is_server_error
129+
assert response.is_error
130+
with pytest.raises(httpx.HTTPStatusError) as exc_info:
101131
response.raise_for_status()
132+
assert str(exc_info.value) == (
133+
"Server error '500 Internal Server Error' for url 'https://example.org'\n"
134+
"For more information check: https://httpstatuses.com/500"
135+
)
102136

103137
# Calling .raise_for_status without setting a request instance is
104138
# not valid. Should raise a runtime error.

0 commit comments

Comments
 (0)