Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error codes #4550

Merged
merged 9 commits into from
Oct 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions docs/api-guide/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -107,82 +107,114 @@ 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()` - Return 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`.

By default this exception results in a response with the HTTP status code "400 Bad Request".

## AuthenticationFailed

**Signature:** `AuthenticationFailed(detail=None)`
**Signature:** `AuthenticationFailed(detail=None, code=None)`

Raised when an incoming request includes incorrect authentication.

By default this exception results in a response with the HTTP status code "401 Unauthenticated", but it may also result in a "403 Forbidden" response, depending on the authentication scheme in use. See the [authentication documentation][authentication] for more details.

## NotAuthenticated

**Signature:** `NotAuthenticated(detail=None)`
**Signature:** `NotAuthenticated(detail=None, code=None)`

Raised when an unauthenticated request fails the permission checks.

By default this exception results in a response with the HTTP status code "401 Unauthenticated", but it may also result in a "403 Forbidden" response, depending on the authentication scheme in use. See the [authentication documentation][authentication] for more details.

## PermissionDenied

**Signature:** `PermissionDenied(detail=None)`
**Signature:** `PermissionDenied(detail=None, code=None)`

Raised when an authenticated request fails the permission checks.

By default this exception results in a response with the HTTP status code "403 Forbidden".

## 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.

By default this exception results in a response with the HTTP status code "404 Not Found".

## 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.

By default this exception results in a response with the HTTP status code "405 Method Not Allowed".

## 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.

By default this exception results in a response with the HTTP status code "406 Not Acceptable".

## 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`.

By default this exception results in a response with the HTTP status code "415 Unsupported Media Type".

## Throttled

**Signature:** `Throttled(wait=None, detail=None)`
**Signature:** `Throttled(wait=None, detail=None, code=None)`

Raised when an incoming request fails the throttling checks.

By default this exception results in a response with the HTTP status code "429 Too Many Requests".

## ValidationError

**Signature:** `ValidationError(detail)`
**Signature:** `ValidationError(detail, code=None)`

The `ValidationError` exception is slightly different from the other `APIException` classes:

Expand Down
6 changes: 3 additions & 3 deletions rest_framework/authtoken/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def validate(self, attrs):
# (Assuming the default `ModelBackend` authentication backend.)
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Must include "username" and "password".')
raise serializers.ValidationError(msg)
raise serializers.ValidationError(msg, code='authorization')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the codes be different from each other here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. We're not trying to identify every possible case, just a categorization. If uses want to have specific codes for those different cases then that's simple enough to do by customizing this.


attrs['user'] = user
return attrs
132 changes: 96 additions & 36 deletions rest_framework/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,61 @@
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList


def _force_text_recursive(data):
def _get_error_details(data, default_code=None):
"""
Descend into a nested data structure, forcing any
lazy translation strings into plain text.
lazy translation strings or strings into `ErrorDetail`.
"""
if isinstance(data, list):
ret = [
_force_text_recursive(item) for item in data
_get_error_details(item, default_code) for item in data
]
if isinstance(data, ReturnList):
return ReturnList(ret, serializer=data.serializer)
return ret
elif isinstance(data, dict):
ret = {
key: _force_text_recursive(value)
key: _get_error_details(value, default_code)
for key, value in data.items()
}
if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer)
return ret
return force_text(data)

text = force_text(data)
code = getattr(data, 'code', default_code)
return ErrorDetail(text, code)


def _get_codes(detail):
if isinstance(detail, list):
return [_get_codes(item) for item in detail]
elif isinstance(detail, dict):
return {key: _get_codes(value) for key, value in detail.items()}
return detail.code


def _get_full_details(detail):
if isinstance(detail, list):
return [_get_full_details(item) for item in detail]
elif isinstance(detail, dict):
return {key: _get_full_details(value) for key, value in detail.items()}
return {
'message': detail,
'code': detail.code
}


class ErrorDetail(six.text_type):
"""
A string-like object that can additionally
"""
code = None

def __new__(cls, string, code=None):
self = super(ErrorDetail, cls).__new__(cls, string)
self.code = code
return self


class APIException(Exception):
Expand All @@ -47,16 +81,35 @@ 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

def get_codes(self):
"""
Return only the code part of the error details.

Eg. {"name": ["required"]}
"""
return _get_codes(self.detail)

def get_full_details(self):
"""
Return both the message & code parts of the error details.

Eg. {"name": [{"message": "This field is required.", "code": "required"}]}
"""
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
Expand All @@ -67,13 +120,21 @@ def __str__(self):

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

def __init__(self, detail):
# For validation errors the 'detail' key is always required.
# The details should always be coerced to a list if not already.
# 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 = _force_text_recursive(detail)

self.detail = _get_error_details(detail, code)

def __str__(self):
return six.text_type(self.detail)
Expand All @@ -82,75 +143,74 @@ def __str__(self):
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):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
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
Expand Down
Loading