Repository: StackStorm/st2

Issue:
API should return proper content type for errors
Currently, if you try to get the list of actions without providing a token on `auth: true` host, you'll get a json formatted error message `{"faultstring": "unauthorized"}`, but headers would say this is `Content-Type: text/plain` preventing most of the clients (incuding st2client.js) from automatically parsing it.

If we respond with json, we need to properly announce that through content-type header.

Place in code likely requiring the change: https://github.com/StackStorm/st2/blob/4f1ac919f6c1220d03e9098e4d6949013331ddc7/st2common/st2common/hooks.py#L138-L141



Discussion:
Good catch.

Will push a fix.


Resolving patch:
diff --git a/st2common/st2common/hooks.py b/st2common/st2common/hooks.py
--- a/st2common/st2common/hooks.py
+++ b/st2common/st2common/hooks.py
@@ -136,15 +136,25 @@ def on_error(self, state, e):
 
     @staticmethod
     def _abort_unauthorized():
-        return webob.Response(json_encode({
+        body = json_encode({
             'faultstring': 'Unauthorized'
-        }), status=401)
+        })
+        headers = {}
+        headers['Content-Type'] = 'application/json'
+        status = httplib.UNAUTHORIZED
+
+        return webob.Response(body=body, status=status, headers=headers)
 
     @staticmethod
     def _abort_other_errors():
-        return webob.Response(json_encode({
+        body = json_encode({
             'faultstring': 'Internal Server Error'
-        }), status=500)
+        })
+        headers = {}
+        headers['Content-Type'] = 'application/json'
+        status = httplib.INTERNAL_SERVER_ERROR
+
+        return webob.Response(body=body, status=status, headers=headers)
 
     @staticmethod
     def _validate_token(request):
diff --git a/st2common/st2common/middleware/auth.py b/st2common/st2common/middleware/auth.py
--- a/st2common/st2common/middleware/auth.py
+++ b/st2common/st2common/middleware/auth.py
@@ -16,6 +16,7 @@
 from six.moves.urllib import parse as urlparse
 
 from st2common.util import isotime
+from st2common.util.jsonify import json_encode
 from st2common.exceptions import auth as exceptions
 from st2common import log as logging
 from st2common.util.auth import validate_token
@@ -56,12 +57,22 @@ def __call__(self, environ, start_response):
             return self.app(environ, start_response)
 
     def _abort_other_errors(self, environ, start_response):
-        start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/plain')])
-        return ['Internal Server Error']
+        body = json_encode({
+            'faultstring': 'Internal Server Error'
+        })
+        headers = [('Content-Type', 'application/json')]
+
+        start_response('500 INTERNAL SERVER ERROR', headers)
+        return [body]
 
     def _abort_unauthorized(self, environ, start_response):
-        start_response('401 UNAUTHORIZED', [('Content-Type', 'text/plain')])
-        return ['Unauthorized']
+        body = json_encode({
+            'faultstring': 'Unauthorized'
+        })
+        headers = [('Content-Type', 'application/json')]
+
+        start_response('401 UNAUTHORIZED', headers)
+        return [body]
 
     def _remove_auth_headers(self, env):
         """Remove middleware generated auth headers to prevent user from supplying them."""


Test patch:
diff --git a/st2api/tests/unit/controllers/v1/test_auth.py b/st2api/tests/unit/controllers/v1/test_auth.py
--- a/st2api/tests/unit/controllers/v1/test_auth.py
+++ b/st2api/tests/unit/controllers/v1/test_auth.py
@@ -42,6 +42,7 @@ class TestTokenValidation(AuthMiddlewareTest):
     def test_token_validation_token_in_headers(self):
         response = self.app.get('/v1/actions', headers={'X-Auth-Token': TOKEN},
                                 expect_errors=False)
+        self.assertTrue('application/json' in response.headers['content-type'])
         self.assertEqual(response.status_int, 200)
 
     @mock.patch.object(
@@ -49,6 +50,7 @@ def test_token_validation_token_in_headers(self):
         mock.Mock(return_value=TokenDB(id=OBJ_ID, user=USER, token=TOKEN, expiry=FUTURE)))
     def test_token_validation_token_in_query_params(self):
         response = self.app.get('/v1/actions?x-auth-token=%s' % (TOKEN), expect_errors=False)
+        self.assertTrue('application/json' in response.headers['content-type'])
         self.assertEqual(response.status_int, 200)
 
     @mock.patch.object(
@@ -57,6 +59,7 @@ def test_token_validation_token_in_query_params(self):
     def test_token_expired(self):
         response = self.app.get('/v1/actions', headers={'X-Auth-Token': TOKEN},
                                 expect_errors=True)
+        self.assertTrue('application/json' in response.headers['content-type'])
         self.assertEqual(response.status_int, 401)
 
     @mock.patch.object(
@@ -64,8 +67,10 @@ def test_token_expired(self):
     def test_token_not_found(self):
         response = self.app.get('/v1/actions', headers={'X-Auth-Token': TOKEN},
                                 expect_errors=True)
+        self.assertTrue('application/json' in response.headers['content-type'])
         self.assertEqual(response.status_int, 401)
 
     def test_token_not_provided(self):
         response = self.app.get('/v1/actions', expect_errors=True)
+        self.assertTrue('application/json' in response.headers['content-type'])
         self.assertEqual(response.status_int, 401)