I currently have some code for a view based on the Django REST Framework.
Ive been using a customer exception class, but ideally I want to use the inbuilt Django REST exceptions.
From the code below I feel this probably not the best or cleanest way to utilize the REST Framework exceptions to its maximum.
Has anyone got any good examples where they are catching issues and returning them cleanly with the REST built in exceptions ?
class JSONResponse(HttpResponse):
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
def queryInput(request):
try:
auth_token = session_id = getAuthHeader(request)
if not auth_token:
return JSONResponse({'detail' : "fail", "error" : "No X-Auth-Token Found", "data" : None}, status=500)
if request.method:
data = JSONParser().parse(request)
serializer = queryInputSerializer(data=data)
if request.method == 'POST':
if serializer.is_valid():
input= serializer.data["input"]
fetchData = MainRunner(input=input,auth_token=auth_token)
main_data = fetchData.main()
if main_data:
return JSONResponse({'detail' : "success", "error" : None, "data" : main_data}, status=201)
return JSONResponse({'detail' : "Unknown Error","error" : True, "data" : None}, status=500)
except Exception as e:
return JSONResponse({'error' : str(e)},status=500)
The Django REST framework provides several built in exceptions, which are mostly subclasses of DRF's APIException.
You can raise exceptions in your view like you normally would in Python:
from rest_framework.exceptions import APIException
def my_view(request):
raise APIException("There was a problem!")
You could also create your own custom exception by inheriting from APIException and setting status_code and default_detail. Some of the built in ones are: ParseError, AuthenticationFailed, NotAuthenticated, PermissionDenied, NotFound, NotAcceptable, ValidationError, etc.
These will then get converted to a Response by the REST Framework's exception handler. Each exception is associated with a status code that is added to the Response. By default the exception handler is set to the built in handler:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}
But you can set it to your own custom exception handler if you want to convert the exceptions yourself by changing this in your settings.py file:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
And then create the custom handler in that location:
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code
return response
You can use the build in DRF exception, just import and raise
from rest_framework.exceptions import ParseError
...
raise ParseError('I already have a status code!')
Related
I'm starting to dive into DRF a little deeper of late, and I was wondering I would like to start customising the error messaging that gets return via the API for incorrect permissions, I'd like to wrap a little extra detail.
For example, if authentication credentials were not provided for an endpoint that is permission restricted, the API returns:
{
"detail": "Authentication credentials were not provided."
}
Which comes from line 171 from the rest_framework.exceptions: https://github.com/encode/django-rest-framework/blob/master/rest_framework/exceptions.py. Really, I'd like this to be consistent with the
{
"success": false,
"message": "Authentication credentials were not provided.",
"data": null
}
So, I assume I now need to begin customising my own exceptions.
How best should I go about doing this?
Perhaps it has some tie in with default_error_messages = {} inside the serializer ...
You can override DRF's default exception handler and JSON parser on your settings.py:
REST_FRAMEWORK = {
...
'EXCEPTION_HANDLER': 'helpers.exceptions.custom_exception_handler',
'DEFAULT_RENDERER_CLASSES': [
'helpers.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]
}
And then it's just a matter of customizing how to handle your exceptions and how to render the responses:
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Customize your exception handling here
return response
And you can use the custom JSON renderer in case you need to do any extra formatting on the response, in my case I had to add a "status_code" to the payload:
class JSONRenderer(BaseJsonRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Render `data` into JSON, returning a bytestring.
"""
<Base code from the original class...>
response = renderer_context.get('response')
if response and 200 <= response.status_code <= 299 and 'status_code' not in response.data:
response.data = Errors.success(response.data)
<Base code from the original class...>
My Errors.success(response.data) was just a simpler way to merge the success status code to the data.
There is a decorator solution that creates custom Response on each type of your exceptions:
# project/api/exceptions.py
from functools import wraps
from rest_framework import status
def handle_exceptions(func):
#wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except AuthCredentialsError as exc:
return Response(
{"message": exc.message},
status=status.HTTP_405_METHOD_NOT_ALLOWED,
)
return wrapper
# project/api/your_code.py
from project.api.exceptions import handle_exceptions
class SomeViewSet():
#handle_exceptions
def create(self, request, *args, **kwargs):
raise AuthCredentialsError("Authentication credentials were not provided")
I have a API endpoint where it will do input validation using rest_framework's serializer.is_valid() where it will return custom error message and response.
serializer = FormSerializer(data=data)
if not serializer.is_valid(raise_exception=False):
return Response({"Failure": "Error"}, status=status.HTTP_400_BAD_REQUEST)
Is it possible to populate validation errors without using the generic response provided by raise_exception=True? I am trying to avoid using the generic response as it will display all the validation errors if there are more than one error.
The response will be something like
return Response(
{
"Failure": "Error",
"Error_list": {"field1": "This field is required"}
},
status=status.HTTP_400_BAD_REQUEST
)
Create a Custom Exception class as,
from rest_framework.exceptions import PermissionDenied
from rest_framework import status
class MyCustomExcpetion(PermissionDenied):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "Custom Exception Message"
default_code = 'invalid'
def __init__(self, detail, status_code=None):
self.detail = detail
if status_code is not None:
self.status_code = status_code
Why I'm inherrited from PermissionDenied exception class ??
see this SO post -- Why DRF ValidationError always returns 400
Then in your serializer, raise exceptions as,
class SampleSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = SampleModel
def validate_age(self, age): # field level validation
if age > 10:
raise MyCustomExcpetion(detail={"Failure": "error"}, status_code=status.HTTP_400_BAD_REQUEST)
return age
def validate(self, attrs): # object level validation
if some_condition:
raise MyCustomExcpetion(detail={"your": "exception", "some_other": "key"}, status_code=status.HTTP_410_GONE)
return attrs
age and name are two fields of SampleModel class
Response will be like this
By using this method,
1. You can customize the JSON Response
2. You can return any status codes
3. You don't need to pass True in serializer.is_valid() method (This is not reccomended)
A simple way is to use one of the exception messages, eg NotFound. See docs
# views.py
from rest_framework.exceptions import NotFound
class myview(viewsets.ModelViewSet):
def perform_create(self, serializer):
raise NotFound("My text here")
That will return a 404 and change the response to your text
HTTP 404 Not Found
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "my text here"
}
You can write custom error handler:
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
response.data['Failure'] = 'Error'
return response
I have an API endpoint that allow users to register an account. I would like to return HTTP 409 instead of 400 for a duplicate username.
Here is my serializer:
from django.contrib.auth.models import User
from rest_framework.serializers import ModelSerializer
class UserSerializer(ModelSerializer):
username = CharField()
def validate_username(self, value):
if User.objects.filter(username=value).exists():
raise NameDuplicationError()
return value
class NameDuplicationError(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = u'Duplicate Username'
When the error is triggered, the response is: {"detail":"Duplicate Username"}. I realised that if I subclass APIException, the key detail is used instead of username.
I want to have this response instead {"username":"Duplicate Username"}
or I would like to specify a status code when raising a ValidationError:
def validate_username(self, value):
if User.objects.filter(username=value).exists():
raise serializers.ValidationError('Duplicate Username',
status_code=status.HTTP_409_CONFLICT)
return value
But this does not work as ValidationError only returns 400.
Is there any other way to accomplish this?
You can raise different exceptions like:
from rest_framework.exceptions import APIException
from django.utils.encoding import force_text
from rest_framework import status
class CustomValidation(APIException):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = 'A server error occurred.'
def __init__(self, detail, field, status_code):
if status_code is not None:self.status_code = status_code
if detail is not None:
self.detail = {field: force_text(detail)}
else: self.detail = {'detail': force_text(self.default_detail)}
you can use this in your serializer like:
raise CustomValidation('Duplicate Username','username', status_code=status.HTTP_409_CONFLICT)
or
raise CustomValidation('Access denied','username', status_code=status.HTTP_403_FORBIDDEN)
By default, raising serializers.ValidationError will return with HTTP_400_BAD_REQUEST
But sometimes we would like to return ValidationError with normal 200 status code, because some libraries on the client side can't parse json response data while response code is not 200.
I tried this. but it's not worked:
raise serializers.ValidationError({'message':'Invalid email address'}, code=200)
So we can do this and it works:
res = serializers.ValidationError({'message':'Invalid email address'})
res.status_code = 200
raise res
Use django-rest-framework custom exception handler http://www.django-rest-framework.org/api-guide/exceptions/
def custom_exception_handler(exc, context=None):
response = exception_handler(exc, context)
if response is not None:
if response.data['detail'] == 'Duplicate Username':
response.data['username'] = response.data.pop('detail')
response.status_code = status.HTTP_409_CONFLICT
return response
To add to Anush Devendra's answer, it seems that raising anything else than a ValidationError will bypass the treatment done by DRF on other fields.
Considering this code from DRF in exceptions.py:
def to_internal_value(self, data):
[...]
for field in fields:
[...]
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
[...]
else:
set_value(ret, field.source_attrs, validated_value)
if errors:
raise ValidationError(errors)
return ret
If you want to have this kind of answer:
{
"my_first_field": [
"The first field had an error."
],
"my_second_field": [
"The second field had an error."
],
}
you need to raise a ValidationError in the validate_<field>() methods.
Note that doing so you won't be able to have a custom error heriting from ValidationError and with a status_code different of 400. Your detail message will be extract and a new ValidationError (with a default 400 status_code) raise.
If I have a error outside the libs of DRF, django send back the HTML of the error instead of the proper error response use by DRF.
For example:
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
print request.POST['tables']
Return the exception MultiValueDictKeyError: "'tables'". And get back the full HTML. How get only the error a JSON?
P.D:
This is the final code:
#api_view(['GET', 'POST'])
def process_exception(request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
return Response({
'error': True,
'content': unicode(exception)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
print exception
return process_exception(request, exception)
One way of returning json would be to catch the exceptions and return proper response (assuming you're using JSONParser as default parser):
from rest_framework.response import Response
from rest_framework import status
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
try:
print request.POST['tables']
except:
return Response({'error': True, 'content': 'Exception!'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'error': False})
UPDATE
For global wise use-case the correct idea would be to put the json response in exception middleware.
You can find example in this blog post.
In your case you need to return DRF response, so if any exception gets raised it will end up in the process_exception:
from rest_framework.response import Response
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
return Response({'error': True, 'content': exception}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
You can replace the default error handlers by specifying a custom handler in your URLConf as documented here
Something like this:
# In urls.py
handler500 = 'my_app.views.api_500'
and:
# In my_app.views
def api_500(request):
response = HttpResponse('{"detail":"An Error Occurred"}', content_type="application/json", status=500)
return response
I hope that helps.
As you can see in the documentation.
All you need to do is configure settings.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.parsers.JSONParser',
),
'EXCEPTION_HANDLER': 'core.views.api_500_handler',
}
And pointing to a view that will recieve (exception, context)
Like this:
from rest_framework.views import exception_handler
...
def api_500_handler(exception, context):
response = exception_handler(exception, context)
try:
detail = response.data['detail']
except AttributeError:
detail = exception.message
response = HttpResponse(
json.dumps({'detail': detail}),
content_type="application/json", status=500
)
return response
My implementation is like this because if a expected rest framework exception is raised, like a 'exceptions.NotFound', exception.message will be empty. Thats why Im first calling to exception_handler of rest framework. If is an expected exception, I will get its message.
I would like to return some JSON responses back instead of just returning a header with an error code. Is there a way in tastypie to handle errors like that?
Figured it out eventually. Here's a good resource to look at, if anyone else needs it. http://gist.github.com/1116962
class YourResource(ModelResource):
def wrap_view(self, view):
"""
Wraps views to return custom error codes instead of generic 500's
"""
#csrf_exempt
def wrapper(request, *args, **kwargs):
try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)
if request.is_ajax():
patch_cache_control(response, no_cache=True)
# response is a HttpResponse object, so follow Django's instructions
# to change it to your needs before you return it.
# https://docs.djangoproject.com/en/dev/ref/request-response/
return response
except (BadRequest, ApiFieldError), e:
return HttpBadRequest({'code': 666, 'message':e.args[0]})
except ValidationError, e:
# Or do some JSON wrapping around the standard 500
return HttpBadRequest({'code': 777, 'message':', '.join(e.messages)})
except Exception, e:
# Rather than re-raising, we're going to things similar to
# what Django does. The difference is returning a serialized
# error message.
return self._handle_500(request, e)
return wrapper
You could overwrite tastypie's Resource method _handle_500(). The fact that it starts with an underscore indeed indicates that this is a "private" method and shouldn't be overwritten, but I find it a cleaner way than having to overwrite wrap_view() and replicate a lot of logic.
This is the way I use it:
from tastypie import http
from tastypie.resources import ModelResource
from tastypie.exceptions import TastypieError
class MyResource(ModelResource):
class Meta:
queryset = MyModel.objects.all()
fields = ('my', 'fields')
def _handle_500(self, request, exception):
if isinstance(exception, TastypieError):
data = {
'error_message': getattr(
settings,
'TASTYPIE_CANNED_ERROR',
'Sorry, this request could not be processed.'
),
}
return self.error_response(
request,
data,
response_class=http.HttpApplicationError
)
else:
return super(MyResource, self)._handle_500(request, exception)
In this case I catch all Tastypie errors by checking if exception is an instance of TastypieError and return a JSON reponse with the message "Sorry, this request could not be processed.". If it's a different exception, I call the parent _handle_500 using super(), which will create a django error page in development mode or send_admins() in production mode.
If you want to have a specific JSON response for a specific exception, just do the isinstance() check on a specific exception. Here are all the Tastypie exceptions:
https://github.com/toastdriven/django-tastypie/blob/master/tastypie/exceptions.py
Actually I think there should be a better/cleaner way to do this in Tastypie, so I opened a ticket on their github.