is_informational / is_success / is_redirect / is_client_error / is_server_error (#1854)

This commit is contained in:
Tom Christie 2021-09-13 13:52:58 +01:00 committed by GitHub
parent ff9813e84d
commit a761e17abc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 33 deletions

View File

@ -945,7 +945,7 @@ class Client(BaseClient):
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)
@ -1640,7 +1640,7 @@ class AsyncClient(BaseClient):
response.history = list(history)
if not response.is_redirect:
if not response.has_redirect_location:
return response
request = self._build_redirect_request(request, response)

View File

@ -1400,22 +1400,79 @@ class Response:
return self._decoder
@property
def is_error(self) -> bool:
return codes.is_error(self.status_code)
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:
return codes.is_redirect(self.status_code) and "location" in self.headers
"""
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 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(
@ -1423,12 +1480,31 @@ class Response:
"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:

View File

@ -39,32 +39,47 @@ class codes(IntEnum):
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"

View File

@ -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.