Skip to content

Commit

Permalink
is_informational / is_success / is_redirect / is_client_error / is_se…
Browse files Browse the repository at this point in the history
…rver_error (#1854)
  • Loading branch information
tomchristie committed Sep 13, 2021
1 parent ff9813e commit a761e17
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 32 deletions.
4 changes: 2 additions & 2 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ def _send_handling_redirects(
hook(response)
response.history = list(history)

if not response.is_redirect:
if not response.has_redirect_location:
return response

request = self._build_redirect_request(request, response)
Expand Down Expand Up @@ -1640,7 +1640,7 @@ async def _send_handling_redirects(

response.history = list(history)

if not response.is_redirect:
if not response.has_redirect_location:
return response

request = self._build_redirect_request(request, response)
Expand Down
102 changes: 89 additions & 13 deletions httpx/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1399,36 +1399,112 @@ def _get_content_decoder(self) -> ContentDecoder:

return self._decoder

@property
def is_informational(self) -> bool:
"""
A property which is `True` for 1xx status codes, `False` otherwise.
"""
return codes.is_informational(self.status_code)

@property
def is_success(self) -> bool:
"""
A property which is `True` for 2xx status codes, `False` otherwise.
"""
return codes.is_success(self.status_code)

@property
def is_redirect(self) -> bool:
"""
A property which is `True` for 3xx status codes, `False` otherwise.
Note that not all responses with a 3xx status code indicate a URL redirect.
Use `response.has_redirect_location` to determine responses with a properly
formed URL redirection.
"""
return codes.is_redirect(self.status_code)

@property
def is_client_error(self) -> bool:
"""
A property which is `True` for 4xx status codes, `False` otherwise.
"""
return codes.is_client_error(self.status_code)

@property
def is_server_error(self) -> bool:
"""
A property which is `True` for 5xx status codes, `False` otherwise.
"""
return codes.is_server_error(self.status_code)

@property
def is_error(self) -> bool:
"""
A property which is `True` for 4xx and 5xx status codes, `False` otherwise.
"""
return codes.is_error(self.status_code)

@property
def is_redirect(self) -> bool:
return codes.is_redirect(self.status_code) and "location" in self.headers
def has_redirect_location(self) -> bool:
"""
Returns True for 3xx responses with a properly formed URL redirection,
`False` otherwise.
"""
return (
self.status_code
in (
# 301 (Cacheable redirect. Method may change to GET.)
codes.MOVED_PERMANENTLY,
# 302 (Uncacheable redirect. Method may change to GET.)
codes.FOUND,
# 303 (Client should make a GET or HEAD request.)
codes.SEE_OTHER,
# 307 (Equiv. 302, but retain method)
codes.TEMPORARY_REDIRECT,
# 308 (Equiv. 301, but retain method)
codes.PERMANENT_REDIRECT,
)
and "Location" in self.headers
)

def raise_for_status(self) -> None:
"""
Raise the `HTTPStatusError` if one occurred.
"""
message = (
"{0.status_code} {error_type}: {0.reason_phrase} for url: {0.url}\n"
"For more information check: https://httpstatuses.com/{0.status_code}"
)

request = self._request
if request is None:
raise RuntimeError(
"Cannot call `raise_for_status` as the request "
"instance has not been set on this response."
)

if codes.is_client_error(self.status_code):
message = message.format(self, error_type="Client Error")
raise HTTPStatusError(message, request=request, response=self)
elif codes.is_server_error(self.status_code):
message = message.format(self, error_type="Server Error")
raise HTTPStatusError(message, request=request, response=self)
if self.is_success:
return

if self.has_redirect_location:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"Redirect location: '{0.headers[location]}'\n"
"For more information check: https://httpstatuses.com/{0.status_code}"
)
else:
message = (
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
"For more information check: https://httpstatuses.com/{0.status_code}"
)

status_class = self.status_code // 100
error_types = {
1: "Informational response",
3: "Redirect response",
4: "Client error",
5: "Server error",
}
error_type = error_types.get(status_class, "Invalid status code")
message = message.format(self, error_type=error_type)
raise HTTPStatusError(message, request=request, response=self)

def json(self, **kwargs: typing.Any) -> typing.Any:
if self.charset_encoding is None and self.content and len(self.content) > 3:
Expand Down
45 changes: 30 additions & 15 deletions httpx/_status_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,32 +39,47 @@ def get_reason_phrase(cls, value: int) -> str:
return ""

@classmethod
def is_redirect(cls, value: int) -> bool:
return value in (
# 301 (Cacheable redirect. Method may change to GET.)
codes.MOVED_PERMANENTLY,
# 302 (Uncacheable redirect. Method may change to GET.)
codes.FOUND,
# 303 (Client should make a GET or HEAD request.)
codes.SEE_OTHER,
# 307 (Equiv. 302, but retain method)
codes.TEMPORARY_REDIRECT,
# 308 (Equiv. 301, but retain method)
codes.PERMANENT_REDIRECT,
)
def is_informational(cls, value: int) -> bool:
"""
Returns `True` for 1xx status codes, `False` otherwise.
"""
return 100 <= value <= 199

@classmethod
def is_error(cls, value: int) -> bool:
return 400 <= value <= 599
def is_success(cls, value: int) -> bool:
"""
Returns `True` for 2xx status codes, `False` otherwise.
"""
return 200 <= value <= 299

@classmethod
def is_redirect(cls, value: int) -> bool:
"""
Returns `True` for 3xx status codes, `False` otherwise.
"""
return 300 <= value <= 399

@classmethod
def is_client_error(cls, value: int) -> bool:
"""
Returns `True` for 4xx status codes, `False` otherwise.
"""
return 400 <= value <= 499

@classmethod
def is_server_error(cls, value: int) -> bool:
"""
Returns `True` for 5xx status codes, `False` otherwise.
"""
return 500 <= value <= 599

@classmethod
def is_error(cls, value: int) -> bool:
"""
Returns `True` for 4xx or 5xx status codes, `False` otherwise.
"""
return 400 <= value <= 599

# informational
CONTINUE = 100, "Continue"
SWITCHING_PROTOCOLS = 101, "Switching Protocols"
Expand Down
38 changes: 36 additions & 2 deletions tests/models/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,49 @@ def test_raise_for_status():
response = httpx.Response(200, request=request)
response.raise_for_status()

# 1xx status codes are informational responses.
response = httpx.Response(101, request=request)
assert response.is_informational
with pytest.raises(httpx.HTTPStatusError) as exc_info:
response.raise_for_status()
assert str(exc_info.value) == (
"Informational response '101 Switching Protocols' for url 'https://example.org'\n"
"For more information check: https://httpstatuses.com/101"
)

# 3xx status codes are redirections.
headers = {"location": "https://other.org"}
response = httpx.Response(303, headers=headers, request=request)
assert response.is_redirect
with pytest.raises(httpx.HTTPStatusError) as exc_info:
response.raise_for_status()
assert str(exc_info.value) == (
"Redirect response '303 See Other' for url 'https://example.org'\n"
"Redirect location: 'https://other.org'\n"
"For more information check: https://httpstatuses.com/303"
)

# 4xx status codes are a client error.
response = httpx.Response(403, request=request)
with pytest.raises(httpx.HTTPStatusError):
assert response.is_client_error
assert response.is_error
with pytest.raises(httpx.HTTPStatusError) as exc_info:
response.raise_for_status()
assert str(exc_info.value) == (
"Client error '403 Forbidden' for url 'https://example.org'\n"
"For more information check: https://httpstatuses.com/403"
)

# 5xx status codes are a server error.
response = httpx.Response(500, request=request)
with pytest.raises(httpx.HTTPStatusError):
assert response.is_server_error
assert response.is_error
with pytest.raises(httpx.HTTPStatusError) as exc_info:
response.raise_for_status()
assert str(exc_info.value) == (
"Server error '500 Internal Server Error' for url 'https://example.org'\n"
"For more information check: https://httpstatuses.com/500"
)

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

0 comments on commit a761e17

Please sign in to comment.