]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
is_informational / is_success / is_redirect / is_client_error / is_server_error ...
authorTom Christie <tom@tomchristie.com>
Mon, 13 Sep 2021 12:52:58 +0000 (13:52 +0100)
committerGitHub <noreply@github.com>
Mon, 13 Sep 2021 12:52:58 +0000 (13:52 +0100)
httpx/_client.py
httpx/_models.py
httpx/_status_codes.py
tests/models/test_responses.py

index 7492cb45f238794f9e80e1a1e5995758a8af8dc2..c4a6e9182c24afb970351b54e957e917642dcfff 100644 (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)
index 7c6460e73fda35560df88061a31334bf5697bd1e..bf99469ca65730842e57e22739051bb864b5b203 100644 (file)
@@ -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:
index 100aec641bf9e17942073562258aa33aa0fbf100..e5004412751353dadd8984f5cbb9d050ea7a6f57 100644 (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"
index 6af06c3c5367eb67710d0bd4cf669b230a67b79a..942231eed4dc554814dcee5cf4036925c4d90795 100644 (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.