From: Tom Christie Date: Mon, 13 Sep 2021 12:52:58 +0000 (+0100) Subject: is_informational / is_success / is_redirect / is_client_error / is_server_error ... X-Git-Tag: 1.0.0.beta0~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a761e17abc9649d62dd1ff4c9df975c5ed1c6f99;p=thirdparty%2Fhttpx.git is_informational / is_success / is_redirect / is_client_error / is_server_error (#1854) --- diff --git a/httpx/_client.py b/httpx/_client.py index 7492cb45..c4a6e918 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -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) diff --git a/httpx/_models.py b/httpx/_models.py index 7c6460e7..bf99469c 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -1399,23 +1399,80 @@ class Response: 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( @@ -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: diff --git a/httpx/_status_codes.py b/httpx/_status_codes.py index 100aec64..e5004412 100644 --- a/httpx/_status_codes.py +++ b/httpx/_status_codes.py @@ -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" diff --git a/tests/models/test_responses.py b/tests/models/test_responses.py index 6af06c3c..942231ee 100644 --- a/tests/models/test_responses.py +++ b/tests/models/test_responses.py @@ -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.