diff options
| -rw-r--r-- | docs/api-guide/exceptions.md | 47 | ||||
| -rw-r--r-- | docs/api-guide/relations.md | 2 | ||||
| -rw-r--r-- | docs/api-guide/settings.md | 16 | ||||
| -rw-r--r-- | docs/topics/credits.md | 2 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 1 | ||||
| -rw-r--r-- | rest_framework/generics.py | 11 | ||||
| -rw-r--r-- | rest_framework/settings.py | 4 | ||||
| -rw-r--r-- | rest_framework/tests/test_generics.py | 42 | ||||
| -rw-r--r-- | rest_framework/tests/test_views.py | 41 | ||||
| -rw-r--r-- | rest_framework/views.py | 2 |
10 files changed, 161 insertions, 7 deletions
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 8b3e50f1..0c48783a 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -28,11 +28,54 @@ For example, the following request: Might receive an error response indicating that the `DELETE` method is not allowed on that resource: HTTP/1.1 405 Method Not Allowed - Content-Type: application/json; charset=utf-8 + Content-Type: application/json Content-Length: 42 - + {"detail": "Method 'DELETE' not allowed."} +## Custom exception handling + +You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. + +The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. + +For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: + + HTTP/1.1 405 Method Not Allowed + Content-Type: application/json + Content-Length: 62 + + {"status_code": 405, "detail": "Method 'DELETE' not allowed."} + +In order to alter the style of the response, you could write the following custom exception handler: + + from rest_framework.views import exception_handler + + def custom_exception_handler(exc): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc) + + # Now add the HTTP status code to the response. + if response is not None: + response.data['status_code'] = response.status_code + + return response + +The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler' + } + +If not specified, the `'EXCEPTION_HANDLER'` setting defaults to the standard exception handler provided by REST framework: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' + } + +Note that the exception handler will only be called for responses generated by raised exceptions. It will not be used for any responses returned directly by the view, such as the `HTTP_400_BAD_REQUEST` responses that are returned by the generic views when serializer validation fails. + --- # API Reference diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 15ba9a3a..5ec4b22f 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -421,7 +421,7 @@ For example, if all your object URLs used both a account and a slug in the the U def get_object(self, queryset, view_name, view_args, view_kwargs): account = view_kwargs['account'] slug = view_kwargs['slug'] - return queryset.get(account=account, slug=sug) + return queryset.get(account=account, slug=slug) --- diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 542e8c5f..13f96f9a 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -25,7 +25,7 @@ If you need to access the values of REST framework's API settings in your projec you should use the `api_settings` object. For example. from rest_framework.settings import api_settings - + print api_settings.DEFAULT_AUTHENTICATION_CLASSES The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. @@ -339,6 +339,20 @@ Default: `'rest_framework.views.get_view_description'` ## Miscellaneous settings +#### EXCEPTION_HANDLER + +A string representing the function that should be used when returning a response for any given exception. If the function returns `None`, a 500 error will be raised. + +This setting can be changed to support error responses other than the default `{"detail": "Failure..."}` responses. For example, you can use it to provide API responses like `{"errors": [{"message": "Failure...", "code": ""} ...]}`. + +This should be a function with the following signature: + + exception_handler(exc) + +* `exc`: The exception. + +Default: `'rest_framework.views.exception_handler'` + #### FORMAT_SUFFIX_KWARG The name of a parameter in the URL conf that may be used to provide a format suffix. diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b2d3d5d2..07e2ec47 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -166,6 +166,7 @@ The following people have helped make REST framework great. * Alexander Akhmetov - [alexander-akhmetov] * Andrey Antukh - [niwibe] * Mathieu Pillard - [diox] +* Edmond Wong - [edmondwong] Many thanks to everyone who's contributed to the project. @@ -368,3 +369,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexander-akhmetov]: https://github.com/alexander-akhmetov [niwibe]: https://github.com/niwibe [diox]: https://github.com/diox +[edmondwong]: https://github.com/edmondwong diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 708aef38..1f363310 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `cache` attribute to throttles to allow overriding of default cache. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 14feed20..7d1bf794 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -356,8 +356,15 @@ class GenericAPIView(views.APIView): self.check_permissions(cloned_request) # Test object permissions if method == 'PUT': - self.get_object() - except (exceptions.APIException, PermissionDenied, Http404): + try: + self.get_object() + except Http404: + # Http404 should be acceptable and the serializer + # metadata should be populated. Except this so the + # outer "else" clause of the try-except-else block + # will be executed. + pass + except (exceptions.APIException, PermissionDenied): pass else: # If user has appropriate permissions for the view, include diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8c084751..8abaf140 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -77,6 +77,9 @@ DEFAULTS = { '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 @@ IMPORT_STRINGS = ( '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/tests/test_generics.py b/rest_framework/tests/test_generics.py index 7a87d389..79cd99ac 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -272,6 +272,48 @@ class TestInstanceView(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected) + def test_options_before_instance_create(self): + """ + OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata + before the instance has been created + """ + request = factory.options('/999') + with self.assertNumQueries(1): + response = self.view(request, pk=999).render() + expected = { + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'renders': [ + 'application/json', + 'text/html' + ], + 'name': 'Instance', + 'description': 'Example description for OPTIONS.', + 'actions': { + 'PUT': { + 'text': { + 'max_length': 100, + 'read_only': False, + 'required': True, + 'type': 'string', + 'label': 'Text comes here', + 'help_text': 'Text description.' + }, + 'id': { + 'read_only': True, + 'required': False, + 'type': 'integer', + 'label': 'ID', + }, + } + } + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + def test_get_instance_view_incorrect_arg(self): """ GET requests with an incorrect pk type, should raise 404, not 500. diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py index c0bec5ae..65c7e50e 100644 --- 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 @@ class FunctionBasedViewIntegrationTests(TestCase): } 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) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4cff0422..853e6461 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -361,7 +361,7 @@ class APIView(View): else: exc.status_code = status.HTTP_403_FORBIDDEN - response = exception_handler(exc) + response = self.settings.EXCEPTION_HANDLER(exc) if response is None: raise |
