Repository: brettcannon/gidgethub (python)

Issue:
Handle case when GitHub doesn't return JSON error
So I've done _something_ wrong and got HTTP 422 (it wasn't obvious from logs, only from checking sansio code...):
```python-traceback
  File "/app/.heroku/python/lib/python3.7/site-packages/gidgethub/abc.py", line 82, in _make_request
    data, self.rate_limit, more = sansio.decipher_response(*response)
  File "/app/.heroku/python/lib/python3.7/site-packages/gidgethub/sansio.py", line 309, in decipher_response
    errors = data.get("errors", None)
AttributeError: 'str' object has no attribute 'get'
```

It would be nice if gidgethub was emitting more logs so that it'd be easier to debug such things.

And, of course, maybe it should process this more gracefully...

P.S. I still don't know what the error response is. Will have to patch some things to get to it.

Pull request description:
Raise a unique exception if an error has no JSON
Closes #81 

Patch:
diff --git a/docs/__init__.rst b/docs/__init__.rst
index b5386f2..9dc8acb 100644
--- a/docs/__init__.rst
+++ b/docs/__init__.rst
@@ -45,6 +45,18 @@ Exceptions
    Inherits from :exc:`HTTPException`.
 
 
+.. exception:: BadRequestUnknownError
+
+   Exception for ``422`` status code responses which do not include a JSON
+   response.
+
+   .. versionadded:: XXX
+
+   .. attribute:: response
+
+      The response as returned by GitHub.
+
+
 .. exception:: RateLimitExceeded(rate_limit)
 
     Raised when one's rate limit has been reached. A subclass of
diff --git a/docs/changelog.rst b/docs/changelog.rst
index b82be09..0b5c7cd 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -7,6 +7,8 @@ Changelog
 .. note::
    Under development
 
+- Add :exc:`gidgethub.BadRequestUnknownError` when something other than JSON is
+  returned for a 422 response.
 - Remove `gidgethub.treq`; tests were not passing and a request for help on
   Twitter came back with no reponse (happy to add back if someone steps forward
   to help out).
diff --git a/gidgethub/__init__.py b/gidgethub/__init__.py
index c89a387..1e07dd2 100644
--- a/gidgethub/__init__.py
+++ b/gidgethub/__init__.py
@@ -43,6 +43,15 @@ class BadRequest(HTTPException):
     # https://developer.github.com/v3/#client-errors
 
 
+class BadRequestUnknownError(BadRequest):
+
+    """A bad request whose response body is not JSON."""
+
+    def __init__(self, response: str) -> None:
+        self.response = response
+        super().__init__(http.HTTPStatus.UNPROCESSABLE_ENTITY)
+
+
 class RateLimitExceeded(BadRequest):
 
     """Request rejected due to the rate limit being exceeded."""
diff --git a/gidgethub/sansio.py b/gidgethub/sansio.py
index 0183c60..0bc5b96 100644
--- a/gidgethub/sansio.py
+++ b/gidgethub/sansio.py
@@ -19,6 +19,7 @@ import uritemplate
 
 from . import (
     BadRequest,
+    BadRequestUnknownError,
     GitHubBroken,
     HTTPException,
     InvalidField,
@@ -329,7 +330,11 @@ def decipher_response(
                 if rate_limit and not rate_limit.remaining:
                     raise RateLimitExceeded(rate_limit, message)
             elif status_code == 422:
-                errors = data.get("errors", None)
+                try:
+                    errors = data.get("errors", None)
+                except AttributeError:
+                    # Not JSON so don't know why the request failed.
+                    raise BadRequestUnknownError(data)
                 exc_type = InvalidField
                 if errors:
                     if any(


Test patch:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index b51b042..ecdb2de 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -2,6 +2,7 @@ import http
 
 from gidgethub import (
     BadRequest,
+    BadRequestUnknownError,
     GitHubBroken,
     HTTPException,
     InvalidField,
@@ -36,6 +37,12 @@ def test_BadRequest():
     assert exc.status_code == http.HTTPStatus.BAD_REQUEST
 
 
+def test_BadRequestUnknownError():
+    exc = BadRequestUnknownError("uh-oh")
+    assert exc.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY
+    assert exc.response == "uh-oh"
+
+
 def test_RateLimitExceeded():
     rate = sansio.RateLimit(limit=1, remaining=0, reset_epoch=1)
     exc = RateLimitExceeded(rate)
diff --git a/tests/test_sansio.py b/tests/test_sansio.py
index 484c222..f77e33e 100644
--- a/tests/test_sansio.py
+++ b/tests/test_sansio.py
@@ -8,6 +8,7 @@ import pytest
 
 from gidgethub import (
     BadRequest,
+    BadRequestUnknownError,
     GitHubBroken,
     HTTPException,
     InvalidField,
@@ -376,6 +377,17 @@ class TestDecipherResponse:
         assert exc_info.value.status_code == http.HTTPStatus(status_code)
         assert str(exc_info.value) == "Reference does not exist"
 
+    def test_422_html_response(self):
+        # https://github.com/brettcannon/gidgethub/issues/81
+        status_code = 422
+        body = "<html><body>Mistakes were made ...</body></html>"
+        encoded_body = body.encode("utf-8")
+        headers = {"content-type": "text/html; charset=utf-8"}
+        with pytest.raises(BadRequestUnknownError) as exc_info:
+            sansio.decipher_response(status_code, headers, encoded_body)
+        assert exc_info.value.status_code == http.HTTPStatus(status_code)
+        assert exc_info.value.response == body
+
     def test_3XX(self):
         status_code = 301
         with pytest.raises(RedirectionException) as exc_info: