I’m trying to create a custom error response from the REST Django framework.
I’ve included the following in my views.py,
from rest_framework.views import exception_handler
def custom_exception_handler(exc):
"""
Custom exception handler for Django Rest Framework that adds
the `status_code` to the response and renames the `detail` key to `error`.
"""
response = exception_handler(exc)
if response is not None:
response.data['status_code'] = response.status_code
response.data['error'] = response.data['detail']
response.data['detail'] = "CUSTOM ERROR"
return response
And also added the following to settings.py.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
'EXCEPTION_HANDLER': 'project.input.utils.custom_exception_handler'
}
Am I missing something as I don’t get the expected response. i.e a custom error message in the 400 API response.
Thanks,
As Bibhas said, with custom exception handlers, you only can return own defined errors when an exception is called. If you want to return a custom response error without exceptions being triggered, you need to return it in the view itself. For example:
return Response({'detail' : "Invalid arguments", 'args' : ['arg1', 'arg2']},
status = status.HTTP_400_BAD_REQUEST)
From the documentation -
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.
Related
I am using django class based view and rest framework
object = self.get_object()
In Detail view if object does not exist and i do get request like
/user/10
then i get this response
{"detail": "not found"}
Now i want to customize that response
like
try:
obj = self.get_object()
except:
raise Exception("This object does not exist")
But thats not working
We can implement a custom exception handler function that returns the custom response in case the object does not exist.
In case a object does not exist, Http404 exception is raised. So, we will check if the exception raised is Http404 and if that is the case, we will return our custom exception message in the response.
from rest_framework.views import exception_handler
from django.http import Http404
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)
if isinstance(exc, Http404):
custom_response_data = {
'detail': 'This object does not exist.' # custom exception message
}
response.data = custom_response_data # set the custom response data on response object
return response
After defining our custom exception handler, we need to add this custom exception handler to our DRF settings.
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
You could create a custom exception class as below, and it would raise APIException with a custom message and custom status_code
from rest_framework.serializers import ValidationError
from rest_framework import status
class CustomAPIException(ValidationError):
"""
raises API exceptions with custom messages and custom status codes
"""
status_code = status.HTTP_400_BAD_REQUEST
default_code = 'error'
def __init__(self, detail, status_code=None):
self.detail = detail
if status_code is not None:
self.status_code = status_code
and in your views,
from rest_framework import status
try:
obj = self.get_object()
except:
raise CustomAPIException("This object does not exist", status_code=status.HTTP_404_NOT_FOUND)
The response will be like this
{"detail": "This object does not exist"}
NOTE
the detail parameter of CustomAPIException class takes str,list and dict objects. If you provide a dict object, then it will return that dict as exception response
UPDATE
As #pratibha mentioned, it's not possible to produce desired output if we use this exception class in Serializer's validate() or validate_xxfieldName() methods.
Why this behaviour ?
I wrote a similar answer in SO, here Django REST Framework ValidationError always returns 400
How to obtain desired output within the serializer's validate() method?
Inherit CustomAPIException from rest_framework.exceptions.APIException instead of from rest_framework.serializers.ValidationError
ie,
from rest_framework.exceptions import APIException
class CustomAPIException(APIException):
# .... code
I am using Django Rest Framework 3.2.3 (DRF) and Django Rest Framework JWT 1.7.2 (DRF-JWT, https://github.com/GetBlimp/django-rest-framework-jwt) to create Login tokens.
I need to change the Status Code for invalid credentials when issuing a JWT from 400 to 202 (FYI: my client can't read the body of non-200 responses). I use a custom exception handler as described by Django Rest Framework to achieve it: http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
restapi/custom_exception.py
from rest_framework.views import exception_handler
from rest_framework import status
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)
print ('Exception raised: ' + str(response.status_code))
# Now add the HTTP status code to the response.
if response is not None:
if response.status_code != status.HTTP_403_FORBIDDEN:
response.status_code = status.HTTP_202_ACCEPTED
return response
And in the config:
'EXCEPTION_HANDLER': 'restapi.custom_exception.custom_exception_handler',
The DRF-JWT should raise ValidationError, when invalid credentials are used. I still get a 400 Bad Request response code, when posting invalid credentials to JWT token-auth interface.
With every other DRF interface I am getting the 202 status code as expected.
How can I get DRF-JWT to use the custom exception handler for their ValidationErrors?
Why we are not able to use custom exception handler here?
This is happening because raise_exception flag has not been passed to the JSONWebTokenSerializer when calling .is_valid() on it. ( JSONWebTokenSerializer is the serializer class used to validate a username and password.)
DRF-JWT source code for post() method:
def post(self, request):
serializer = self.get_serializer(
data=get_request_data(request)
)
if serializer.is_valid(): # 'raise_exception' flag has not been passed here
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
return Response(response_data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Now, we can see that raise_exception flag has not been passed to is_valid(). When this happens, ValidationError is not raised thereby resulting in non-execution of your custom_exception_handler code.
As per the DRF section on Raising an exception on invalid data:
The .is_valid() method takes an optional raise_exception flag that
will cause it to raise a serializers.ValidationError exception if
there are validation errors.
These exceptions are automatically dealt with by the default exception
handler that REST framework provides, and will return HTTP 400 Bad
Request responses by default.
SOLUTION:
If you pass the raise_exception flag as True when calling the .is_valid() function, the code for custom_exception_handler will be executed.
You will need to create a CustomObtainJSONWebToken view which will inherit from the default ObtainJSONWebToken view. In this, we will override the .post() method to pass the raise_exception flag. Then will specify this view in our urls.
my_app/views.py
from rest_framework_jwt.views import ObtainJSONWebToken
class CustomObtainJSONWebToken(ObtainJSONWebToken):
def post(self, request):
serializer = self.get_serializer(
data=get_request_data(request)
)
serializer.is_valid(raise_exception=True) # pass the 'raise_exception' flag
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
return Response(response_data)
urls.py
# use this view to obtain token
url(r'^api-token-auth/', CustomObtainJSONWebToken.as_view())
I think maybe a cleaner way to do it that doesn't involve overriding the post method is
serializers.py
from rest_framework_jwt import serializers as jwt_serializers
class JSONWebTokenSerializer(jwt_serializers.JSONWebTokenSerializer):
"""
Override rest_framework_jwt's ObtainJSONWebToken serializer to
force it to raise ValidationError exception if validation fails.
"""
def is_valid(self, raise_exception=None):
"""
If raise_exception is unset, set it to True by default
"""
return super().is_valid(
raise_exception=raise_exception if raise_exception is not
None else True)
views.py
from rest_framework_jwt import views as jwt_views
from .serializers import JSONWebTokenSerializer
class ObtainJSONWebToken(jwt_views.ObtainJSONWebToken):
"""
Override the default JWT ObtainJSONWebToken view to use the custom serializer
"""
serializer_class = JSONWebTokenSerializer
urls.py
from django.conf.urls import url
from .views import ObtainJSONWebToken
urlpatterns = [
...
url(r'^api-token-auth/', ObtainJSONWebToken.as_view(), name='jwt-create'),
]
Well, at this moment I know how to make a custom error 404 page (and it actually works in my app). I followed the same strategy step by step to make a 403 page, but to no avail. So, this is what I have done:
#root urls.py
handler403 = 'blog.views.custom_403'
#blog views.py
def custom_403(request):
template = loader.get_template('blog/403.html')
context = RequestContext(request,{})
return HttpResponse(template.render(context))
Then, in one of my functions I have this code:
return HttpResponseForbidden()
I thought, it would render 403.html, but what I see is just a message 403 FORBIDDEN in the console.
The trouble is, you are not raising a 403 exception (as you did for the 404), but simply returning a response with a status code of 403. As the docs show, you need to raise django.views.defaults.permission_denied to trigger the 403 page.
from django.core.exceptions import PermissionDenied
...
raise PermissionDenied
I am not sure how can I catch an Exception in my django app.
I have a view, where I use my own class object which uses Requests framework to make remote API requests.
I need to catch Timeout exception inside my class code. When Timeout exception appears I need to return custom error page. My class code:
try:
response = requests.request(
method=method, url=self.url, auth=self.auth, timeout=3, verify=False, **kwargs
)
except requests.Timeout:
return HttpResponse('API connection timeout')
return response
However, in my django view code, I use something like:
user_details = auth_client['auth/users/%s/details' % (request.POST.get('username'))].get().json()['result']
so, when I try to return HttpResponse from class code, I get error:
'HttpResponse' object has no attribute 'json'
I don't know how can I "omit" code from view and show HttpResponse directly. Any suggestions?
You shouldn't be trying to return an HttpResponse in your requests class. Instead of catching the exception and replacing it with an unwanted HttpResponse, let it propagate and catch it in the view:
try:
user_details = auth_client['auth/users/%s/details' % (request.POST.get('username'))].get().json()['result']
except requests.Timeout:
return HttpResponse('whatever')
Edit OK, so here's another solution. You say that this is a class: so you can define a method, say get_user_details, which either returns the actual JSON, or none.
def get_user_details(username):
try:
user_details = self['auth/users/%s/details' % 'username'].get()
except requests.Timeout:
return []
return user_details.json()['result']
so now in your view you just call auth_client.get_user_details(request.POST['username']).
I have got JSON API (as the part of the project) implemented with Django.
Sometimes I return error to user in JSON form. I want to notify admin via standard error reporting procedure via email (for example when uncaught exception raised). But I want to return some JSON response too, not 500 error page.
Some metacode to make sure that everything clear:
def request_handler(request):
try:
code_that_rises_an_exception()
return response('{"result":"success"}')
except Exception,e:
notify_admin_about_error(e) # <- this function is required
return response('{"result":"error"}')
Thank you!
You can use Django Middleware for this. Middlewares allow you to modify/process Django's HttpRequest object that is supplied to a view and HttpResponse object that is returned by a view as well as take action when a view raises an exception. You can do variety of tasks with this such as logging of the meta data of the requests you receive, error reporting etc. Django calls process_exception whenever an exception is raised by a view and you can define process_exception() to send a mail to you whenever an exception gets raised.
class ErrorReportingMiddleware(object):
def process_exception(self, request, exception):
send_mail_to_admin(exception) # you can collect some more information here
return HttpResponse('{"result":"error"}') # this response is returned to the user
Add this class to your MIDDLEWARE_CLASSES variable in settings.py at the end of that tuple.
Your view would reduce to:
def request_handler(request):
code_that_rises_an_exception()
return response('{"result":"success"}')
Now if an exception is raised by request_handler, Django would call process_exception method of your ErrorReportingMiddleware which will send a mail to the admin about the exception and return a JSON response to the browser instead of a 500 page. I would implement send_mail_to_admin as an asynchronous function so that the processing of the response by django doesn't get blocked due to sending of mail and quick response is returned to the user.