Repository: auth0/auth0-python
Pull request #92 (2018-02-08 18:26:57)

Correctly throw an exception when handing a text response
Hello. We've seen this problem in our running system when calling `GetToken.client_credentials()`. We currently have to parse the response text to see if it looks like an error message rather than a token.


Patch:
diff --git a/auth0/v3/authentication/base.py b/auth0/v3/authentication/base.py
index 778912c..7d466b3 100644
--- a/auth0/v3/authentication/base.py
+++ b/auth0/v3/authentication/base.py
@@ -2,6 +2,7 @@ import json
 import requests
 from ..exceptions import Auth0Error
 
+UNKNOWN_ERROR = 'a0.sdk.internal.unknown'
 
 class AuthenticationBase(object):
 
@@ -14,13 +15,64 @@ class AuthenticationBase(object):
         return requests.get(url=url, params=params, headers=headers).text
 
     def _process_response(self, response):
+        return self._parse(response).content()
+
+    def _parse(self, response):
+        if not response.text:
+            return EmptyResponse(response.status_code)
         try:
-            text = json.loads(response.text) if response.text else {}
+            return JsonResponse(response)
         except ValueError:
-            return response.text
+            return PlainResponse(response)
+
+class Response(object):
+    def __init__(self, status_code, content):
+        self._status_code = status_code
+        self._content = content
+
+    def content(self):
+        if self._is_error():
+            raise Auth0Error(status_code=self._status_code,
+                             error_code=self._error_code(),
+                             message=self._error_message())
+        else:
+            return self._content
+
+    def _is_error(self):
+        return self._status_code is None or self._status_code >= 400
+
+class JsonResponse(Response):
+    def __init__(self, response):
+        content = json.loads(response.text)
+        super().__init__(response.status_code, content)
+
+    def _error_code(self):
+        if 'error' in self._content:
+            return self._content.get('error')
+        elif 'code' in self._content:
+            return self._content.get('code')
         else:
-            if response.status_code is None or response.status_code >= 400:
-                raise Auth0Error(status_code=response.status_code,
-                                 error_code=text.get('error', ''),
-                                 message=text.get('error_description', ''))
-        return text
+            return UNKNOWN_ERROR
+
+    def _error_message(self):
+        return self._content.get('error_description', '')
+
+class PlainResponse(Response):
+    def __init__(self, response):
+        super().__init__(response.status_code, response.text)
+
+    def _error_code(self):
+        return UNKNOWN_ERROR
+
+    def _error_message(self):
+        return self._content
+
+class EmptyResponse(Response):
+    def __init__(self, status_code):
+        super().__init__(status_code, '')
+
+    def _error_code(self):
+        return UNKNOWN_ERROR
+
+    def _error_message(self):
+        return ''


Test patch:
diff --git a/auth0/v3/test/authentication/test_base.py b/auth0/v3/test/authentication/test_base.py
index d6539c8..03187f1 100644
--- a/auth0/v3/test/authentication/test_base.py
+++ b/auth0/v3/test/authentication/test_base.py
@@ -35,3 +35,65 @@ class TestBase(unittest.TestCase):
             self.assertEqual(context.exception.status_code, error_status)
             self.assertEqual(context.exception.error_code, 'e0')
             self.assertEqual(context.exception.message, 'desc')
+
+    @mock.patch('requests.post')
+    def test_post_error_with_code_property(self, mock_post):
+        ab = AuthenticationBase()
+
+        for error_status in [400, 500, None]:
+            mock_post.return_value.status_code = error_status
+            mock_post.return_value.text = '{"code": "e0",' \
+                                          '"error_description": "desc"}'
+
+            with self.assertRaises(Auth0Error) as context:
+                data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'})
+
+            self.assertEqual(context.exception.status_code, error_status)
+            self.assertEqual(context.exception.error_code, 'e0')
+            self.assertEqual(context.exception.message, 'desc')
+
+    @mock.patch('requests.post')
+    def test_post_error_with_no_error_code(self, mock_post):
+        ab = AuthenticationBase()
+
+        for error_status in [400, 500, None]:
+            mock_post.return_value.status_code = error_status
+            mock_post.return_value.text = '{"error_description": "desc"}'
+
+            with self.assertRaises(Auth0Error) as context:
+                data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'})
+
+            self.assertEqual(context.exception.status_code, error_status)
+            self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown')
+            self.assertEqual(context.exception.message, 'desc')
+
+    @mock.patch('requests.post')
+    def test_post_error_with_text_response(self, mock_post):
+        ab = AuthenticationBase()
+
+        for error_status in [400, 500, None]:
+            mock_post.return_value.status_code = error_status
+            mock_post.return_value.text = 'there has been a terrible error'
+
+            with self.assertRaises(Auth0Error) as context:
+                data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'})
+
+            self.assertEqual(context.exception.status_code, error_status)
+            self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown')
+            self.assertEqual(context.exception.message,
+                             'there has been a terrible error')
+
+    @mock.patch('requests.post')
+    def test_post_error_with_no_response_text(self, mock_post):
+        ab = AuthenticationBase()
+
+        for error_status in [400, 500, None]:
+            mock_post.return_value.status_code = error_status
+            mock_post.return_value.text = None
+
+            with self.assertRaises(Auth0Error) as context:
+                data = ab.post('the-url', data={'a': 'b'}, headers={'c': 'd'})
+
+            self.assertEqual(context.exception.status_code, error_status)
+            self.assertEqual(context.exception.error_code, 'a0.sdk.internal.unknown')
+            self.assertEqual(context.exception.message, '')