Apply error codes to all exception types. Add documentation.

This commit is contained in:
Tom Christie 2016-10-11 10:09:00 +01:00
parent 2a3d07d9d3
commit e4b961bd1b
2 changed files with 100 additions and 64 deletions

View File

@ -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`. 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: 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): class ServiceUnavailable(APIException):
status_code = 503 status_code = 503
default_detail = 'Service temporarily unavailable, try again later.' 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 ## ParseError
**Signature:** `ParseError(detail=None)` **Signature:** `ParseError(detail=None, code=None)`
Raised if the request contains malformed data when accessing `request.data`. 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 ## AuthenticationFailed
**Signature:** `AuthenticationFailed(detail=None)` **Signature:** `AuthenticationFailed(detail=None, code=None)`
Raised when an incoming request includes incorrect authentication. 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 ## NotAuthenticated
**Signature:** `NotAuthenticated(detail=None)` **Signature:** `NotAuthenticated(detail=None, code=None)`
Raised when an unauthenticated request fails the permission checks. 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 ## PermissionDenied
**Signature:** `PermissionDenied(detail=None)` **Signature:** `PermissionDenied(detail=None, code=None)`
Raised when an authenticated request fails the permission checks. 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 ## 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. 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 ## 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. 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 ## 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. 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 ## 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`. 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 ## Throttled
**Signature:** `Throttled(wait=None, detail=None)` **Signature:** `Throttled(wait=None, detail=None, code=None)`
Raised when an incoming request fails the throttling checks. Raised when an incoming request fails the throttling checks.

View File

@ -81,43 +81,19 @@ class APIException(Exception):
""" """
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = _('A server error occurred.') default_detail = _('A server error occurred.')
default_code = 'error'
def __init__(self, detail=None): def __init__(self, detail=None, code=None):
if detail is not None: if detail is None:
self.detail = force_text(detail) detail = self.default_detail
else: if code is None:
self.detail = force_text(self.default_detail) code = self.default_code
self.detail = _get_error_details(detail, code)
def __str__(self): def __str__(self):
return self.detail 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): def get_codes(self):
""" """
Return only the code part of the error details. Return only the code part of the error details.
@ -135,65 +111,95 @@ class ValidationError(APIException):
return _get_full_details(self.detail) 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): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Malformed request.') default_detail = _('Malformed request.')
default_code = 'parse_error'
class AuthenticationFailed(APIException): class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = _('Incorrect authentication credentials.') default_detail = _('Incorrect authentication credentials.')
default_code = 'authentication_failed'
class NotAuthenticated(APIException): class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = _('Authentication credentials were not provided.') default_detail = _('Authentication credentials were not provided.')
default_code = 'not_authenticated'
class PermissionDenied(APIException): class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = _('You do not have permission to perform this action.') default_detail = _('You do not have permission to perform this action.')
default_code = 'permission_denied'
class NotFound(APIException): class NotFound(APIException):
status_code = status.HTTP_404_NOT_FOUND status_code = status.HTTP_404_NOT_FOUND
default_detail = _('Not found.') default_detail = _('Not found.')
default_code = 'not_found'
class MethodNotAllowed(APIException): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = _('Method "{method}" not allowed.') default_detail = _('Method "{method}" not allowed.')
default_code = 'method_not_allowed'
def __init__(self, method, detail=None): def __init__(self, method, detail=None, code=None):
if detail is not None: if detail is None:
self.detail = force_text(detail) detail = force_text(self.default_detail).format(method=method)
else: super(MethodNotAllowed, self).__init__(detail, code)
self.detail = force_text(self.default_detail).format(method=method)
class NotAcceptable(APIException): class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = _('Could not satisfy the request Accept header.') default_detail = _('Could not satisfy the request Accept header.')
default_code = 'not_acceptable'
def __init__(self, detail=None, available_renderers=None): def __init__(self, detail=None, code=None, available_renderers=None):
if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers self.available_renderers = available_renderers
super(NotAcceptable, self).__init__(detail, code)
class UnsupportedMediaType(APIException): class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = _('Unsupported media type "{media_type}" in request.') default_detail = _('Unsupported media type "{media_type}" in request.')
default_code = 'unsupported_media_type'
def __init__(self, media_type, detail=None): def __init__(self, media_type, detail=None, code=None):
if detail is not None: if detail is None:
self.detail = force_text(detail) detail = force_text(self.default_detail).format(media_type=media_type)
else: super(UnsupportedMediaType, self).__init__(detail, code)
self.detail = force_text(self.default_detail).format(
media_type=media_type
)
class Throttled(APIException): class Throttled(APIException):
@ -201,12 +207,10 @@ class Throttled(APIException):
default_detail = _('Request was throttled.') default_detail = _('Request was throttled.')
extra_detail_singular = 'Expected available in {wait} second.' extra_detail_singular = 'Expected available in {wait} second.'
extra_detail_plural = 'Expected available in {wait} seconds.' extra_detail_plural = 'Expected available in {wait} seconds.'
default_code = 'throttled'
def __init__(self, wait=None, detail=None): def __init__(self, wait=None, detail=None, code=None):
if detail is not None: super(Throttled, self).__init__(detail, code)
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
if wait is None: if wait is None:
self.wait = None self.wait = None