diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index d0dda070f..133c519a5 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -98,7 +98,7 @@ Note that the exception handler will only be called for responses generated by r The **base class** for all exceptions raised inside an `APIView` class or `@api_view`. -To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class. +To provide a custom exception, subclass `APIException` and set the `.status_code`, `.default_detail`, and `default_code` attributes on the class. For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so: @@ -107,10 +107,42 @@ For example, if your API relies on a third party service that may sometimes be u class ServiceUnavailable(APIException): status_code = 503 default_detail = 'Service temporarily unavailable, try again later.' + default_code = 'service_unavailable' + +#### Inspecting API exceptions + +There are a number of different properties available for inspecting the status +of an API exception. You can use these to build custom exception handling +for your project. + +The available attributes and methods are: + +* `.detail` - Return the textual description of the error. +* `.get_codes()` - Return the code identifier of the error. +* `.full_details()` - Retrun both the textual description and the code identifier. + +In most cases the error detail will be a simple item: + + >>> print(exc.detail) + You do not have permission to perform this action. + >>> print(exc.get_codes()) + permission_denied + >>> print(exc.full_details()) + {'message':'You do not have permission to perform this action.','code':'permission_denied'} + +In the case of validation errors the error detail will be either a list or +dictionary of items: + + >>> print(exc.detail) + {"name":"This field is required.","age":"A valid integer is required."} + >>> print(exc.get_codes()) + {"name":"required","age":"invalid"} + >>> print(exc.get_full_details()) + {"name":{"message":"This field is required.","code":"required"},"age":{"message":"A valid integer is required.","code":"invalid"}} ## ParseError -**Signature:** `ParseError(detail=None)` +**Signature:** `ParseError(detail=None, code=None)` Raised if the request contains malformed data when accessing `request.data`. @@ -118,7 +150,7 @@ By default this exception results in a response with the HTTP status code "400 B ## AuthenticationFailed -**Signature:** `AuthenticationFailed(detail=None)` +**Signature:** `AuthenticationFailed(detail=None, code=None)` Raised when an incoming request includes incorrect authentication. @@ -126,7 +158,7 @@ By default this exception results in a response with the HTTP status code "401 U ## NotAuthenticated -**Signature:** `NotAuthenticated(detail=None)` +**Signature:** `NotAuthenticated(detail=None, code=None)` Raised when an unauthenticated request fails the permission checks. @@ -134,7 +166,7 @@ By default this exception results in a response with the HTTP status code "401 U ## PermissionDenied -**Signature:** `PermissionDenied(detail=None)` +**Signature:** `PermissionDenied(detail=None, code=None)` Raised when an authenticated request fails the permission checks. @@ -142,7 +174,7 @@ By default this exception results in a response with the HTTP status code "403 F ## NotFound -**Signature:** `NotFound(detail=None)` +**Signature:** `NotFound(detail=None, code=None)` Raised when a resource does not exists at the given URL. This exception is equivalent to the standard `Http404` Django exception. @@ -150,7 +182,7 @@ By default this exception results in a response with the HTTP status code "404 N ## MethodNotAllowed -**Signature:** `MethodNotAllowed(method, detail=None)` +**Signature:** `MethodNotAllowed(method, detail=None, code=None)` Raised when an incoming request occurs that does not map to a handler method on the view. @@ -158,7 +190,7 @@ By default this exception results in a response with the HTTP status code "405 M ## NotAcceptable -**Signature:** `NotAcceptable(detail=None)` +**Signature:** `NotAcceptable(detail=None, code=None)` Raised when an incoming request occurs with an `Accept` header that cannot be satisfied by any of the available renderers. @@ -166,7 +198,7 @@ By default this exception results in a response with the HTTP status code "406 N ## UnsupportedMediaType -**Signature:** `UnsupportedMediaType(media_type, detail=None)` +**Signature:** `UnsupportedMediaType(media_type, detail=None, code=None)` Raised if there are no parsers that can handle the content type of the request data when accessing `request.data`. @@ -174,7 +206,7 @@ By default this exception results in a response with the HTTP status code "415 U ## Throttled -**Signature:** `Throttled(wait=None, detail=None)` +**Signature:** `Throttled(wait=None, detail=None, code=None)` Raised when an incoming request fails the throttling checks. diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index a76e493c7..e41655fef 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -81,43 +81,19 @@ class APIException(Exception): """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = _('A server error occurred.') + default_code = 'error' - def __init__(self, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, detail=None, code=None): + if detail is None: + detail = self.default_detail + if code is None: + code = self.default_code + + self.detail = _get_error_details(detail, code) def __str__(self): return self.detail - -# The recommended style for using `ValidationError` is to keep it namespaced -# under `serializers`, in order to minimize potential confusion with Django's -# built in `ValidationError`. For example: -# -# from rest_framework import serializers -# raise serializers.ValidationError('Value was invalid') - -class ValidationError(APIException): - status_code = status.HTTP_400_BAD_REQUEST - - def __init__(self, detail, code=None): - # For validation errors the 'detail' key is always required. - # The details should always be coerced to a list if not already. - if not isinstance(detail, dict) and not isinstance(detail, list): - detail = [detail] - - if code is None: - default_code = 'invalid' - else: - default_code = code - - self.detail = _get_error_details(detail, default_code) - - def __str__(self): - return six.text_type(self.detail) - def get_codes(self): """ Return only the code part of the error details. @@ -135,65 +111,95 @@ class ValidationError(APIException): return _get_full_details(self.detail) +# The recommended style for using `ValidationError` is to keep it namespaced +# under `serializers`, in order to minimize potential confusion with Django's +# built in `ValidationError`. For example: +# +# from rest_framework import serializers +# raise serializers.ValidationError('Value was invalid') + +class ValidationError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Invalid input.') + default_code = 'invalid' + + def __init__(self, detail, code=None): + if detail is None: + detail = self.default_detail + if code is None: + code = self.default_code + + # For validation failures, we may collect may errors together, so the + # details should always be coerced to a list if not already. + if not isinstance(detail, dict) and not isinstance(detail, list): + detail = [detail] + + self.detail = _get_error_details(detail, code) + + def __str__(self): + return six.text_type(self.detail) + + class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Malformed request.') + default_code = 'parse_error' class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Incorrect authentication credentials.') + default_code = 'authentication_failed' class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Authentication credentials were not provided.') + default_code = 'not_authenticated' class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = _('You do not have permission to perform this action.') + default_code = 'permission_denied' class NotFound(APIException): status_code = status.HTTP_404_NOT_FOUND default_detail = _('Not found.') + default_code = 'not_found' class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED default_detail = _('Method "{method}" not allowed.') + default_code = 'method_not_allowed' - def __init__(self, method, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail).format(method=method) + def __init__(self, method, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail).format(method=method) + super(MethodNotAllowed, self).__init__(detail, code) class NotAcceptable(APIException): status_code = status.HTTP_406_NOT_ACCEPTABLE default_detail = _('Could not satisfy the request Accept header.') + default_code = 'not_acceptable' - def __init__(self, detail=None, available_renderers=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, detail=None, code=None, available_renderers=None): self.available_renderers = available_renderers + super(NotAcceptable, self).__init__(detail, code) class UnsupportedMediaType(APIException): status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE default_detail = _('Unsupported media type "{media_type}" in request.') + default_code = 'unsupported_media_type' - def __init__(self, media_type, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail).format( - media_type=media_type - ) + def __init__(self, media_type, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail).format(media_type=media_type) + super(UnsupportedMediaType, self).__init__(detail, code) class Throttled(APIException): @@ -201,12 +207,10 @@ class Throttled(APIException): default_detail = _('Request was throttled.') extra_detail_singular = 'Expected available in {wait} second.' extra_detail_plural = 'Expected available in {wait} seconds.' + default_code = 'throttled' - def __init__(self, wait=None, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, wait=None, detail=None, code=None): + super(Throttled, self).__init__(detail, code) if wait is None: self.wait = None