Repository: encode/django-rest-framework

Issue:
Customizable exception handler
Please take a look.

I did not touch docs yet. Also, in Python 3 there is some weird test failure caused by catch_warnings()

Main ideas:
1. Depreciate returning Response with status_code >= 400. Exception should be raised instead;
2. Make exception handler customizable via settings



Discussion:
Okay.  A lot of this looks good, but I'm not really happy with raising exceptions instead of returning responses.
That's too big of a change for me to be comfortable with, and it just doesn't feel right.
I'd accept something along these lines if it strictly only dealt with existing exceptions, although I realize that might not do 100% of what you're looking for in your use case.  If you really need to modify 400 error responses and you don't want to override the existing views you might need to look into doing that in middleware or something similar.

No, middleware is bad idea. In this case it's better to write a couple of custom mixins and assemble custom views in my project.

I think the only thing you may consider to reject in this code is raising warning when user tries to return non-4xx error code. I agree that this is too much of restriction.

I needed something similar here — to add a custom error code. I ended up creating a mixin that overrides finalise_response.

Roughly: check the response, update it accordingly, call the super implementation — It seemed to work okay. 

On 4 Jun 2013, at 21:31, Tom Christie notifications@github.com wrote:

> Okay. A lot of this looks good, but I'm not really happy with raising exceptions instead of returning responses.
> That's too big of a change for me to be comfortable with, and it just doesn't feel right.
> I'd accept something along these lines if it strictly only dealt with existing exceptions, although I realize that might not do 100% of what you're looking for in your use case. If you really need to modify 400 error responses and you don't want to override the existing views you might need to look into doing that in middleware or something similar.
> 
> —
> Reply to this email directly or view it on GitHub.

Yeah, agree that mixin approach is nicer than middleware.
Either way, the point about not wanting to change the current 400 responses into exceptions still stands - that just doesn't feel right to me.

Ok will remote that and fix tests...

See the following thread... https://groups.google.com/d/msg/django-rest-framework/ndgmIU8kfOY/M_tMmAiPhp8J

This is now easier, as we just need to switch the current hardcoded exception handler function out into a setting... https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/views.py#L364

Marked for attention at DjangoCon.  My previous comment and links in there should provide sufficient context to get going on this, essentially just switch out the hardcoded exception handler function into a configurable setting.

Hi Tom,

Sorry I was too busy these months, and could not dedicate time to this PR...

@avsd - totally fine, nothing to apologise for :)


Resolving patch:
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -77,6 +77,9 @@
     'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
     'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
 
+    # Exception handling
+    'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
+
     # Testing
     'TEST_REQUEST_RENDERER_CLASSES': (
         'rest_framework.renderers.MultiPartRenderer',
@@ -125,6 +128,7 @@
     'DEFAULT_MODEL_SERIALIZER_CLASS',
     'DEFAULT_PAGINATION_SERIALIZER_CLASS',
     'DEFAULT_FILTER_BACKENDS',
+    'EXCEPTION_HANDLER',
     'FILTER_BACKEND',
     'TEST_REQUEST_RENDERER_CLASSES',
     'UNAUTHENTICATED_USER',
diff --git a/rest_framework/views.py b/rest_framework/views.py
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -361,7 +361,7 @@ def handle_exception(self, exc):
             else:
                 exc.status_code = status.HTTP_403_FORBIDDEN
 
-        response = exception_handler(exc)
+        response = self.settings.EXCEPTION_HANDLER(exc)
 
         if response is None:
             raise


Test patch:
diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py
--- a/rest_framework/tests/test_views.py
+++ b/rest_framework/tests/test_views.py
@@ -32,6 +32,16 @@ def basic_view(request):
         return {'method': 'PATCH', 'data': request.DATA}
 
 
+class ErrorView(APIView):
+    def get(self, request, *args, **kwargs):
+        raise Exception
+
+
+@api_view(['GET'])
+def error_view(request):
+    raise Exception
+
+
 def sanitise_json_error(error_dict):
     """
     Exact contents of JSON error messages depend on the installed version
@@ -99,3 +109,34 @@ def test_400_parse_error_tunneled_content(self):
         }
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(sanitise_json_error(response.data), expected)
+
+
+class TestCustomExceptionHandler(TestCase):
+    def setUp(self):
+        self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
+
+        def exception_handler(exc):
+            return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
+
+        api_settings.EXCEPTION_HANDLER = exception_handler
+
+    def tearDown(self):
+        api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
+
+    def test_class_based_view_exception_handler(self):
+        view = ErrorView.as_view()
+
+        request = factory.get('/', content_type='application/json')
+        response = view(request)
+        expected = 'Error!'
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(response.data, expected)
+
+    def test_function_based_view_exception_handler(self):
+        view = error_view
+
+        request = factory.get('/', content_type='application/json')
+        response = view(request)
+        expected = 'Error!'
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(response.data, expected)