I am putting together an API with Django Rest Framework. I want to customise my error handling. I read quite a bit (link1, link2, link3) about custom error handling but can't find something that suits my needs.
Basically, I'd like to change the structure of my error messages to get something like this :
{
"error": True,
"errors": [
{
"message": "Field %s does not exist",
"code": 1050
}
]
}
Instead of :
{"detail":"Field does not exist"}
I already have a custom ExceptionMiddleware to catch the 500 errors and return a JSON, but I have no power on all the other errors.
Code of the ExceptionMiddleware:
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
if request.user.is_staff:
detail = exception.message
else:
detail = 'Something went wrong, please contact a staff member.'
return HttpResponse('{"detail":"%s"}'%detail, content_type="application/json", status=500)
From Django doc :
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.
This is exactly what I am trying to achieve, customise those 400 errors.
Thanks a lot,
I know this is a bit late, (better late than never).
If you have a structured error message, then try this by inheriting the Exception class
from rest_framework.serializers import ValidationError
from rest_framework import status
class CustomAPIException(ValidationError):
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 the usage will be like this:
if some_condition:
error_msg = {
"error": True,
"errors": [
{
"message": "Field %s does not exist"%('my_test_field'),
"code": 1050
}
]
}
raise CustomAPIException(error_msg)
Reference : How to override exception messages in django rest framework
This is my custom exception handler:
def api_exception_handler(exception, context):
if isinstance(exception, exceptions.APIException):
headers = {}
if getattr(exception, 'auth_header', None):
headers['WWW-Authenticate'] = exception.auth_header
if getattr(exception, 'wait', None):
headers['Retry-After'] = '%d' % exception.wait
data = exception.get_full_details()
set_rollback()
return Response(data, status=exception.status_code, headers=headers)
return exception_handler(exception, context)
It represents APIException errors in a format like this:
{
"field_name": [
{
"message": "Error message",
"code": "error_code"
},
{
"message": "Another error",
"code": "another_error"
}
]
}
Django Rest Framework reference documentation:
http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
The exception handler is indeed what you're looking for. The current mixing do raise an exception in case of failed validation (https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py).
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.
I think this part doesn't hold any longer and should be rephrased by removing the "generic" word.
Alright so based on Linovia's answer, here is my custom handler :
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
errors = []
for msg in response.data.values():
errors.append({'message': msg[0], 'error': get_error_message()})
response.data = {"errors": errors}
return response
Related
I'm currently learning Django Rest Framework and currently stuck on a problem which is kind of impossible for me to figure out.
I'm providing you an example, just to make the picture clearer.
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import BasePermission
class CheckAuthentication(BasePermission):
def has_permission(self, request, view):
authenticated = request.auth
if not authenticated:
raise PermissionDenied(
{
"exception_data": None
}
)
return True
Ok So, in the above example I want to response to be like the following JSON
{
"exception_data": null
}
but instead of it, I'm getting
{
"exception_data": "None"
}
is there any way I can get the desired JSON ??
You can use json package, for ex:
>>> import json
>>> d = {'t':None}
>>> json.dumps(d)
'{"t": null}'
This can be done via custom exception handling
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
exception_data = str(exc.detail['exception_data'])
response.data['exception_data'] = None if exception_data == "None" else exception_data
return response
and then in your settings.py you have to mention the custom_exception_handler, like the following
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
I have a simple Users resource with a put method to update all user information except user password. According to Flask-Restx docs when a model has set the strict and validation params to true, a validation error will be thrown if an unspecified param is provided in the request. However, this doesn't seem to be working for me.
Model definition:
from flask_restx import Namespace, Resource, fields, marshal
users_ns = Namespace("users")
user = users_ns.model(
"user",
{
"user_name": fields.String(example="some_user", required=True),
"email": fields.String(example="some.user#email", required=True),
"is_admin": fields.Boolean(example="False"),
"is_deactivated": fields.Boolean(example="False"),
"created_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"last_modified_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"uri": fields.Url("api.user"),
},
strict=True,
)
user_post = users_ns.inherit(
"user_post", user, {"password": fields.String(required=True)}
) # Used for when
Resource and method definition:
from api.models import Users
class User(Resource):
#users_ns.marshal_with(user)
#users_ns.expect(user, validate=True)
def put(self, id):
"""
Update a specified user.
"""
user = Users.query.get_or_404(id)
body = request.get_json()
user.update(body)
return user
Failing Test:
def test_update_user_invalid_password_param(self, client, db):
""" User endpoint should return 400 when user attempts to pass password param to update. """
data = {
"user_name": "some_user",
"email": "some.user#email.com",
"password": "newpassword",
}
response = client.put(url_for("api.user", id=1), json=data)
assert response.status_code == 400
The response.status_code here is 200 because no validation error is thrown for the unspecified param passed in the body of the request.
Am I using the strict param improperly? Am I misunderstanding the behavior of strict?
UPDATED: I've added the test for strict model param from Flask-RestX repo (can be found here) for more context on expected behavior:
def test_api_payload_strict_verification(self, app, client):
api = restx.Api(app, validate=True)
ns = restx.Namespace("apples")
api.add_namespace(ns)
fields = ns.model(
"Person",
{
"name": restx.fields.String(required=True),
"age": restx.fields.Integer,
"birthdate": restx.fields.DateTime,
},
strict=True,
)
#ns.route("/validation/")
class Payload(restx.Resource):
payload = None
#ns.expect(fields)
def post(self):
Payload.payload = ns.payload
return {}
data = {
"name": "John Doe",
"agge": 15, # typo
}
resp = client.post_json("/apples/validation/", data, status=400)
assert re.match("Additional properties are not allowed \(u*'agge' was unexpected\)", resp["errors"][""])
I resolved my issue by pulling the latest version of Flask-RESTX from Github. The strict parameter for models was merged after Flask-RESTX version 0.2.0 was released on Pypi in March of 2020 (see the closed issue in Flask-RESTX repo for more context). My confusion arose because the documentation appears to represent the latest state of master and not the last Pypi release.
It's been a while since I touched on this but from what I can tell, I don't think you are using the strict param correctly. From the documentation here, the :param bool strict is defined as
:param bool strict: validation should raise an error when there is param not provided in schema
But in your last snippet of code, you are trying to validate with the dictionary data with the body of the request.
If I recall well again, for this sort of task you need to use (and as you mentioned) a RequestParser. There's a good example of it here - flask - something more strict than #api.expect for input data?
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
What would be an efficient way to construct error messages during HTTP request parameter validation and serialize them?
Currently, as I have it:
except Section.DoesNotExist:
return Response(headers = {'INTERNAL_MSG': 'SECTION_NOT_FOUND',
'INTERNAL_CODE': '400'},
status = status.HTTP_200_OK)
But, it seems to me this is a not a good way to do it, since I am injecting internal error messages into the HTTP protocol that have nothing to do with HTTP topology.
Preferably, I would like to do something like Twitter, Facebook, and others are doing in their APIs:
{"errors":[{"message":"Sorry, that page does not exist","code":34}]}
So, could you please share your approach to dealing with errors and returning them?
Very much appreciated, thanks!
Django REST framework handles errors internally and in general does a pretty good job of converting them into reasonable responses that can be parsed by clients. This is done by the internal exception handling, which is open to being extended for special cases.
Usually when you run into a case where an object was specified that does not exist, you raise a 404 Not Found error. Django actually has a helper for this, called get_object_or_404, as it tends to be a very common bit of logic, especially in APIs. Django REST framework will convert this Http404 error that is raised and convert it in to the following response that has a 404 HTTP error code.
{
"detail": "Not found"
}
Now, this format (and object with a detail key) is not restricted to only Http404 errors, it works for any error that subclasses APIError provided by Django REST framework.
So in your example, you could raise a similar exception by doing
ex = APIError("Section not found")
ex.status_code = 404
raise ex
Or by using get_object_or_404 and letting Django REST framework handle it
from django.shortcuts import get_object_or_404
section = get_object_or_404(Section.objects, pk=1234)
Of course, you can still override the exception handler to format these errors how you want.
def exception_handler(exc):
from django.http import Http404
from rest_framework import status
exc_data = {
"errors": [],
}
if isinstance(exc, Http404):
exc_data["errors"].append({
"message": "Sorry that page does not exist",
"code": 404,
})
else if isinstance(exc.detail, list):
for message in exc.detail:
exc_data["errors"].append({
"message": message,
"code": exc.status_code,
}
else:
exc_data["errors"].append({
"message": exc.detail,
"code": exc.status_code,
})
return Response(exc_data, status=status.HTTP_200_OK)
Note that this will give all error responses a status code of 200, and embed the actual HTTP status code within the body. This is useful for some applications, but it is usually recommended to only use the 200 status code for success responses.
i want to customize the error response from django rest framework.
this is slandered error response from django rest..
{
"style": [
"This field is required."
],
"code": [
"code must be in 60 to 120 chars."
]
}
i want to customize it like....
{
"style": [
"error_code":"20"
"error_message":"This field is required."
],
"code": [
"error_code":"22"
"error_message":"code must be in 60 to 120 chars."
]
}
I had the same problem in showing errors. First of all you should know that you can't use key-value pairs in a list (i.e. "style": ["error_code": "20", "error_message": "This field is required."]) and you should change your type to dictionary if you want to convert your error to this custom type. One easy way is that you can have your own custom exception handler and then tell rest framework to use that exception handler to customize your exceptions. First you should add the following line in your project settings.py:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.exception_handlers.custom_exception_handler', # You should add the path to your custom exception handler here.
}
Which tells that all of the exceptions should pass through this handler. After that you should add python file and add your codes in it (use this file path in your settings.py which mentioned before). In following code you can see an example of this handler:
from rest_framework.exceptions import ErrorDetail
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.
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
custom_data = {}
if isinstance(response.data, dict):
for key, value in response.data.items():
if value and isinstance(value, list) and isinstance(value[0], ErrorDetail):
custom_response[key] = {
"error_message": str(value[0]),
"error_code": response.status_code # or any custom code that you need
}
else:
break
if custom_data:
response.data = custom_data
return response
Note: This was a quick example and you should test your APIs to make sure that everything works.