I'm new to django rest_framework, And tried to create customized error reponse in django!.
Django Rest Framework Exceptions
After going through that, all seems to be pretty straight forward, but the import error raise when i try to add the custom exception.
Settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'project.restapi.utils.custom_exception_handler'
}
ImportError
Exception Value:
Could not import 'project.restapi.utils.custom_exception_handler' for API setting 'EXCEPTION_HANDLER'. AttributeError: module 'project.restapi.utils' has no attribute 'custom_exception_handler'
custom_exception_handler.py
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
model.py
class Users(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
# call the original 'list' to get the original response
response = super(Users, self).retrieve(request, *args, **kwargs)
response.data = {"collection": {"data": response.data},"statusCode":response.status_code,"version":"1.0"}
# customize the response data
if response is not None:
return response
else:
# return response with this custom representation
response.data = {"collection": {"data": response.data},"statusCode":response.status_code,"version":"1.0","error":response.exception}
return response
So on the above model works fine, except when i try to hit the user which is not in the database should raise error - not found, so i tried to customise the not found to be meaningful to my own. that's it
I tried to sort out, but hard to do so!!,
Django Version: 2.1.5
Python - 3.7.0
Since your custom_exception_handler resides in a file named custom_exception_handler.py. You can try changing the EXCEPTION_HANDLER setting to this:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'project.restapi.utils.custom_exception_handler.custom_exception_handler'
}
Related
when I try to delete Django model object that is ForeignKey in another model with option on_delete=models.PROTECT, the error returned is the normal Django 500 Exception HTML web page, how to make Django rest frame work return json response with the error, is there a way for DRF to do that by default or it should be customized?
Raising 500 Exception is the expected behavior. You have to customize to have a custom error. You can refer to this similar question
You can catch the error by using ProtectedError expceptions.
Like this-
from django.db.models import ProtectedError
try:
# Write here your code
except ProtectedError:
# Return your customer validation error
To complete MSI Shafik answer, specifically for django-rest-framework, you can override the destroy method to catch the exception and return protected objects in a response to handle an error message in your application.
For example:
from django.db.models.deletion import ProtectedError
def destroy(self, request, *args, **kwargs):
try:
return super().destroy(request, *args, **kwargs)
except ProtectedError as protected_error:
protected_elements = [
{"id": protected_object.pk, "label": str(protected_object)}
for protected_object in protected_error.protected_objects
]
response_data = {"protected_elements": protected_elements}
return Response(data=response_data, status=status.HTTP_400_BAD_REQUEST)
I have the fields refundable and refundable_price in my model. I need to be sure that there is refundable_price not None in case refundable is True.
Since I want it everywhere, I've overridden SubOffer.clean method:
from django.core.exceptions import ValidationError
def save(self, **kwargs):
self.full_clean()
super().save(**kwargs)
def clean(self):
super().clean()
if self.refundable and self.refundable_price is None:
raise ValidationError("V prípade refundovateľnej ponuky je nutné zadať sumu (je možné zadať aj 0)")
And I use ModelViewSet.
class SubOfferViewSet(ModelViewSet):
serializer_class = SubOfferSerializer
filterset_fields = {
# 'approved_by': ['exact'],
# 'approved_dt': ['gte', 'lte', 'gt', 'lt'],
}
def get_queryset(self):
return SubOffer.objects.all()
The weird thing is that when I send POST to the ViewSet it returns 500 instead of JSON errors if there is an error in Suboffer.clean. Other errors work correctly.
It's the same when I use AJAX and when I use DRF API Viewer.
How is that possible and how to make it work properly?
A clean way that will handle all of your Validation errors (and any other errors you might want) is to have a custom EXCEPTION_HANDLER that converts the Django
ValidationError to a DRF one.
See:
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError as DRFValidationError
from rest_framework.serializers import as_serializer_error
from rest_framework.views import exception_handler as drf_exception_handler
def exception_handler(exc, context):
if isinstance(exc, DjangoValidationError):
exc = DRFValidationError(as_serializer_error(exc))
return drf_exception_handler(exc, context)
"""
In settings:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'path.to_module.drf.exception_handler',
}
"""
Source: https://gist.github.com/twidi/9d55486c36b6a51bdcb05ce3a763e79f
A more in-depth example: https://github.com/HackSoftware/Django-Styleguide-Example/blob/9761c7592af553084e95bb5f8f9407a173aac66f/styleguide_example/api/exception_handlers.py
By defaul DRF handle only APIException (see source). Since you are rising Django'sValidationErrorinstead of DRF'sValidation` error, this handler returns None.
So to fix this, you can use ValidationError from DRF:
from rest_framework.exceptions import ValidationError
or which is better to write your own custom exception_handler::
from rest_framework.views import exception_handler
from django.core.exceptions import ValidationError
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 response is None and isinstance(exc, ValidationError):
return Response(status=400)
return response
I am trying to get into Django atomic transactions at first time. I wonder if there is a possibility to use it something like this:
class TaskViewSet(MultiSerializerViewSet):
#transaction.atomic
#action(methods=['PATCH'], detail=True)
def move_task(self, request, pk):
# making queries, trying to update them, roll back if last one fails.
return Response("message: SUCCESS", status=_status.HTTP_200_OK)
I have searched a bit - there is some information about how to use transactions but I did not find any info if it is possible to use them with DRF.
class PayViewSet(ModelViewSet):
#action(methods=['PATCH'], detail=True)
#transaction.atomic
def approval(self, request, *args, **kwargs):
sid = transaction.savepoint()
success = something
if success:
transaction.savepoint_commit(sid)
return success_response('yes')
else:
transaction.savepoint_rollback(sid)
return error_response('no')
savepoint is optionally depending on your situation
If you need to wrap all API calls and DRF actions into transactions - you can use (ATOMIC_REQUEST) Django database settings [from Django 1.8]
DATABASES = {
"default": {
....
"ATOMIC_REQUESTS": True,
If you use a custom exception handler (DRF Issue) you may need to adjust it like below
REST_FRAMEWORK = {
....
"EXCEPTION_HANDLER": "api.exceptions.your_exception_handler",
}
import logging
from django.db import IntegrityError
from rest_framework.response import Response
from rest_framework.views import exception_handler, set_rollback
def your_exception_handler(exc, context):
"""
DRF custom exception handler. This will allow us to add useful information
to any exceptions to help out our frontend devs
"""
# Call REST framework's default exception handler first, to get the standard error response.
response = exception_handler(exc, context)
if not response and isinstance(exc, IntegrityError):
# https://github.com/encode/django-rest-framework/issues/5760
if 'duplicate key value violates unique constraint' in str(exc):
set_rollback()
msg = "Unique constraint violated: {exc}".format(exc=exc)
response = Response({"error": True, "content": msg}, status=400)
if response is None:
set_rollback()
logger.exception(exc)
response = Response({"error": True, "content": str(exc)}, status=500)
return response
My API allows access (any request) to certain objects only when a user is authenticated and certain other conditions are satisfied.
class SomethingViewSet(viewsets.ModelViewSet):
queryset = Something.objects.filter(query_hiding_non_authorized_objects)
serializer_class = SomethingSerializer
permission_classes = (permissions.IsAuthenticated, SomePermission)
If a user attempts to view a non-authorized object DRF returns a 403 error, however, this gives away that an object with the requested id exists. How can I return 404 errors in these cases?
Note: I also use a custom queryset to hide non-authorized objects from being listed.
As you already hide them in get_queryset just removing the permission will net you a 404 instead.
edit: you can also override the permission_denied method in your View class to throw another exception, this is the default implementation:
def permission_denied(self, request, message=None):
""" If request is not permitted, determine what kind of exception to raise.
"""
if request.authenticators and not request.successful_authenticator:
raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied(detail=message)
I think you can use custom exception handler in this case,
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response.status_code == 403:
response.status_code = 404
return response
In settings:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER':
'my_project.my_app.utils.custom_exception_handler'
}
Second Method
from rest_framework.exceptions import APIException
class CustomForbidden(APIException):
status_code = status.HTTP_404_NOT_FOUND
class CustomPermission(permissions.BasePermission):
def has_permission(self, request, view):
if not_allowed:
raise CustomForbidden
Tech used:
http://www.django-rest-framework.org
Exceptions: http://www.django-rest-framework.org/api-guide/exceptions/
Included rest_framework default example in custom exceptions.py file:
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = request
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This sends basic error info like "Http404" etc, but no request data, like ip address, etc.
Best way to add my request into the response? Thanks in advance.
UPDATE (and solved):
So, I was initially trying to solve this using DjangoRestFramework 2.4.x, but that version doesn't have the request or context data options for the custom exception handler. Upgrading to 3.1.3 made it easy to add the data into the response. New code now looks like this (using version 3.1.3):
def custom_exception_handler(exc, request):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, request)
# Send error to rollbar
rollbar.report_exc_info(sys.exc_info(), request)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This should work for your case.
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = context['request']
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
You can access the request from the context passed to the custom_exception_handler. This was added in DRF 3.1.0. Also refer this issue where it was resolved.
If you are using DRF<3.1, there would be no request in the context of exception handler. You can upgrade to DRF 3.1.3(latest version in PyPI) and then easily access the request in context.
Taken from DRF 3.1.1 source code:
def get_exception_handler_context(self):
"""
Returns a dict that is passed through to EXCEPTION_HANDLER,
as the `context` argument.
"""
return {
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None)
}
Also, you need to configure the exception handler in your settings.py file.
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
If it is not specified, the 'EXCEPTION_HANDLER' setting defaults to the standard exception handler provided by REST framework:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}
Note:
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.