Repository: hikari-py/hikari
Pull request #1121 (2022-04-01 19:23:15)

Don't error on out-of-spec http status codes.
### Summary
These can happen during outages due to Cloudflare's usage of custom status codes.

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [ ] I have run `nox` and all the pipelines have passed.
- [ ] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->



Patch:
diff --git a/changes/1121.bugfix.md b/changes/1121.bugfix.md
new file mode 100644
index 00000000..67440ca3
--- /dev/null
+++ b/changes/1121.bugfix.md
@@ -0,0 +1,2 @@
+Don't error on an out-of-spec HTTP status code (e.g one of Cloudflare's custom status codes).
+`HTTPResponseError.status` may now be of type `http.HTTPStatus` or `int`.
diff --git a/hikari/errors.py b/hikari/errors.py
index 4273d489..470bb553 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -231,8 +231,12 @@ class HTTPResponseError(HTTPError):
     url: str = attr.field()
     """The URL that produced this error message."""
 
-    status: http.HTTPStatus = attr.field()
-    """The HTTP status code for the response."""
+    status: typing.Union[http.HTTPStatus, int] = attr.field()
+    """The HTTP status code for the response.
+
+    This will be `int` if it's outside the range of status codes in the HTTP
+    specification (e.g. one of Cloudflare's non-standard status codes).
+    """
 
     headers: data_binding.Headers = attr.field()
     """The headers received in the error response."""
@@ -247,8 +251,12 @@ class HTTPResponseError(HTTPError):
     """The error code."""
 
     def __str__(self) -> str:
-        name = self.status.name.replace("_", " ").title()
-        name_value = f"{name} {self.status.value}"
+        if isinstance(self.status, http.HTTPStatus):
+            name = self.status.name.replace("_", " ").title()
+            name_value = f"{name} {self.status.value}"
+
+        else:
+            name_value = f"Unknown Status {self.status}"
 
         if self.code:
             code_str = f" ({self.code})"
diff --git a/hikari/internal/net.py b/hikari/internal/net.py
index 3024f513..4e0f0cd8 100644
--- a/hikari/internal/net.py
+++ b/hikari/internal/net.py
@@ -62,7 +62,10 @@ async def generate_error_response(response: aiohttp.ClientResponse) -> errors.HT
     if response.status == http.HTTPStatus.NOT_FOUND:
         return errors.NotFoundError(*args)
 
-    status = http.HTTPStatus(response.status)
+    try:
+        status: typing.Union[http.HTTPStatus, int] = http.HTTPStatus(response.status)
+    except ValueError:
+        status = response.status
 
     if 400 <= status < 500:
         return errors.ClientHTTPResponseError(real_url, status, response.headers, raw_body)


Test patch:
diff --git a/tests/hikari/internal/test_net.py b/tests/hikari/internal/test_net.py
index 4d7312fb..62891653 100644
--- a/tests/hikari/internal/test_net.py
+++ b/tests/hikari/internal/test_net.py
@@ -70,6 +70,52 @@ async def json(self):
     assert returned is error()
 
 
+@pytest.mark.parametrize(
+    ("status_", "expected_error"),
+    [
+        # The following internal server non-conforming status codes are used by cloudflare.
+        # Source I made it up...
+        # jk https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+        (520, "InternalServerError"),
+        (521, "InternalServerError"),
+        (522, "InternalServerError"),
+        (523, "InternalServerError"),
+        (524, "InternalServerError"),
+        (525, "InternalServerError"),
+        (526, "InternalServerError"),
+        (527, "InternalServerError"),
+        (530, "InternalServerError"),
+        # These non-conforming bad requests status codes are sent by NGINX.
+        # Same source as cloudflare status codes.
+        (494, "ClientHTTPResponseError"),
+        (495, "ClientHTTPResponseError"),
+        (496, "ClientHTTPResponseError"),
+        (497, "ClientHTTPResponseError"),
+        # This non-conforming status code is made up.
+        (694, "HTTPResponseError"),
+    ],
+)
+@pytest.mark.asyncio()
+async def test_generate_error_response_with_non_conforming_status_code(status_, expected_error):
+    class StubResponse:
+        real_url = "https://some.url"
+        status = status_
+        headers = {}
+
+        async def read(self):
+            return "some raw body"
+
+        async def json(self):
+            return {"message": "raw message", "code": 123}
+
+    with mock.patch.object(errors, expected_error) as error:
+        returned = await net.generate_error_response(StubResponse())
+
+    error.assert_called_once_with("https://some.url", status_, {}, "some raw body")
+
+    assert returned is error()
+
+
 @pytest.mark.parametrize(
     ("status_", "expected_error"),
     [
diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py
index 4d6d9558..9b71a109 100644
--- a/tests/hikari/test_errors.py
+++ b/tests/hikari/test_errors.py
@@ -87,6 +87,10 @@ def error(self):
     def test_str(self, error):
         assert str(error) == "Bad Request 400: (12345) 'message' for https://some.url"
 
+    def test_str_when_int_status_code(self, error):
+        error.status = 699
+        assert str(error) == "Unknown Status 699: (12345) 'message' for https://some.url"
+
     def test_str_when_message_is_None(self, error):
         error.message = None
         assert str(error) == "Bad Request 400: (12345) 'raw body' for https://some.url"