How do i flatten error messages to display a single consistent error message in Django? - django

I am making a mobile app that uses Django Rest Framework. When one of my models fails validation, it uses one of the model.attributes as a key inside the error message, for example:
{'status_code': 400, 'name': [u'Ensure this field has no more than 32 characters.']}
{'status_code': 400, 'password': [u'Ensure this field has no more than 32 characters.']}
{'status_code': 400, 'arbitrary_field': [u'Ensure this field has no more than 32 characters.']}
This is very difficult to scale in a mobile, so I want to take the error messages and deliver a consistent 'error' key to the mobile device. For example,
{'status_code': 400, 'error': [u' Name: Ensure this field has no more than 32 characters.']}
{'status_code': 400, 'error': [u'Password: Ensure this field has no more than 32 characters.']}
{'status_code': 400, 'error': [u'Arbitrary Field: Ensure this field has no more than 32 characters.']}
In Rails, I could do this by saying:
model.errors.full_messages
But I'm not sure what the equivalent in Django is?
Thanks

You can define a property custom_full_errors in your serializer which will return the errors formatted according to your requirement. Doing serializer.custom_full_errors will give you the desired response.
We first call the serializer.errors to get the default errors dictionary. Then we iterate on this dictionary and create our desired response.
class MySerializer(serializers.ModelSerializer):
#property
def custom_full_errors(self):
"""
Returns full errors formatted as per requirements
"""
default_errors = self.errors # default errors dict
errors_messages = []
for field_name, field_errors in default_errors.items():
for field_error in field_errors:
error_message = '%s: %s'%(field_name, field_error)
errors_messages.append(error_message) # append error message to 'errors_messages'
return {'error': errors_messages}
...
For example:
# serializer.errors
{'name': [u'Ensure this field has no more than 32 characters.']}
will translate to
# serializer.custom_full_errors
{'error': [u'Name: Ensure this field has no more than 32 characters.']}
In case of multiple errors for a single field, we will get the following output on doing serializer.custom_full_errors:
# serializer.custom_full_errors
{'error': [u'Name: Error1', u'Name: Error2', u'Password: Error1', u'Password: Error2' ]}

I read this blog article, am yet to test it out
https://masnun.com/2015/11/06/django-rest-framework-custom-exception-handler.html
You create a custom error handler that concatenates the key-value pairs, after passing the exception through the default error handler method
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)
if response is not None:
data = response.data
response.data = {}
errors = []
for field, value in data.items():
errors.append("{} : {}".format(field, " ".join(value)))
response.data['errors'] = errors
response.data['status'] = False
response.data['exception'] = str(exc)
return response
Then you point DRF to your custom error handler
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'EXCEPTION_HANDLER': 'api.utils.custom_exception_handler'
}

In case somebody is still looking for a solution, here is a great library that helps a lot in dealing with custom error validation: https://pypi.org/project/drf-standardized-errors/, here is the GitHub repo: https://github.com/ghazi-git/drf-standardized-errors
The response will be flat and instead of this:
{
"shipping_address":
{
"non_field_errors": [ErrorDetail("We do not support shipping to the provided address.", code="unsupported")]
}
}
You'll get this:
{
"code": "unsupported",
"detail": "We do not support shipping to the provided address.",
"attr": "shipping_address.non_field_errors"
}
It deals pretty good with List Serializers, changing such response:
{
"recipients": [
{"name": [ErrorDetail("This field is required.", code="required")]},
{"email": [ErrorDetail("Enter a valid email address.", code="invalid")]},
]
}
To this:
{
"type": "validation_error",
"errors": [
{
"code": "required",
"detail": "This field is required.",
"attr": "recipients.0.name"
},
{
"code": "invalid",
"detail": "Enter a valid email address.",
"attr": "recipients.1.email"
}
]
}
Based on this flat response it's much easier to form one string message to represent all the errors if you need to.
Basically, it handles the hardest part - getting a flat response out of a complex Django structure (all the nested fields, dicts, lists, etc.)

I know it is a bit late but I made this one using backtracking in case you have a lot of embedded nested serialisers.
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)
stack = [response.data]
response.data = {}
errors = {}
if response is not None:
while len(stack) > 0:
data = stack.pop()
for field, value in data.items():
if type(value) is dict:
stack.append(value)
else:
if value is not list:
errors[field] = value
continue
for val in value:
code = val.code
message = str(val)
errors[field] = {
'message': message,
'code': code
}
response.data['errors'] = errors
return response

Related

Using Django Bad Request

I'm new in Django and Rest Framework and I didn't find how to do that:
Filter an endpoint request without argument to return Bad Request. Example:
get_foo/?foo_id=
Return:
{
"status": 400,
"error": "Bad Request"
}
At this time, a get request without argument gives all the values from the database. It's a big DB, so I have to do this filter.
You could filter it so If you have an empty queryset you will get that response:
If foo_id == '' # or If not foo_id
return Response(
{"res": "Bad Request"},
status=status.HTTP_404_NOT_FOUND)
else:
return . . .
In your resposne you can also put your text
If foo_id == '' # or If not foo_id
return Response({
"status": 400,
"error": "Bad Request"})

PKCE Parameters for Snapchat

I'm attempting to write a django-allauth Provider for Snapchat and I'm stuck at a roadblock.
Snapchat requires PKCE parameters. I first changed the AUTH_PARAMS.
'AUTH_PARAMS': {
'code_challenge': 'state',
'code_challenge_method': "S256"
}
This has only resulted in invalid responses from the Snapchat API upon Access_Token Request after I have the code response.
This error the first error I got.
{'error': 'invalid_request', 'error_description': 'Invalid code_verifier length.', 'state': ''}
After overriding the SocialLogin.stash_state I receive this error.
{'error': 'invalid_grant', 'error_description': 'Invalid code_verifier.', 'state': ''}
From what I can dig through the code of all auth I can't find anything in the codebase on the PKCE parameters or base64 Url SHA256 encoding.
I'm willing to implement the solution but I'm stuck finding where to subclass the state parameters then match them after.
There are some issues around the Snapchat Docs with this as well.
https://gist.github.com/CisarJosh/733bb76a13f36f0a7944f05d257bb3f6
This is a gist of some of my attempts.
I think this will get you started:
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
import secrets
import base64
import hashlib
import urllib.parse
VOCAB = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~0123456789"
def generate_code_verifier() -> str:
length = max(43, secrets.randbelow(129))
return "".join([secrets.choice(VOCAB) for i in range(0, length)])
def generate_state() -> str:
return secrets.token_urlsafe(32)
class SnapchatProvider(OAuth2Provider):
def get_auth_params(self, request, action):
params = super().get_auth_params(request, action)
code_verifier = generate_code_verifier()
state = generate_state()
# store this state token somewhere so it can be looked up
challenge = hashlib.sha256(code_verifier).digest()
encoded = base64.b64encode(challenge)
urlencoded = urllib.parse.quote_plus(encoded)
params.update(state=state, code_challenge=urlencoded)
return params
That's my interpretation of the spec for that part.

Django Webhook receive post request for Mailgun

I've setup a webhook in Django to receive updates from Mailgun.
The mailgun POST payload is delivered to the webhook in the below format:
{
“signature”:
{
"timestamp": "1529006854",
"token": "a8ce0edb2dd8301dee6c2405235584e45aa91d1e9f979f3de0",
"signature": "d2271d12299f6592d9d44cd9d250f0704e4674c30d79d07c47a66f95ce71cf55"
}
“event-data”:
{
"event": "opened",
"timestamp": 1529006854.329574,
"id": "DACSsAdVSeGpLid7TN03WA",
// ...
}
}
If I try and retrieve event parameter using the below code, I get an error saying TypeError: 'method' object is not subscriptable
#csrf_exempt
#require_POST
def mailgun(request):
event_data = request.POST.get['event-data']['event']
return HttpResponse(event_data, status=200)
Any help is appreciated.
After a lot of troubleshooting, the answer was hidden in the StackOverflow link below. In Python 3.0 to Python 3.5.x, json.loads() will only accept a unicode string, so you must decode request.body (which is a byte string) before passing it to json.loads().
Trying to parse `request.body` from POST in Django
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
content = body['event-data']
recipient = content['recipient']
Try to update .get to call function and add some input checks, ie:
if request.POST.get('event-data'):
event_data = request.POST.get('event-data')['event']
return HttpResponse(event_data, status=200)
else:
return HttpResponse("[unknown event]", status=400)
or if you sure event-data never be empty or null call directly:
request.POST['event-data']['event']

Test cases for Django Rest Framework; struggling to get a correct response

Update: Solved my own problem: I've learnt that Django creates its own test database, and as such, it needs to be populated with data. Ran my importer in my test cases and it all worked. So, if you're also wondering why you're tests don't work, check that you've got some data in the test db!
End Update
I am writing tests for my Django Rest Framework API but I am struggling to get my code to return a 200 OK. At the moment, my test case continually returns a 404 Not Found.
I'm in the early stages of writing tests, and have a lot to learn. I'm currently following https://www.django-rest-framework.org/api-guide/testing/
I'm trying to test an endpoint at the following URL
# Not shown here, is that all URLs here will be prepended with /api/v1
path('case/<int:pk>/', EntireCaseView.as_view(), name='case'),
I have an object in my database with an ID (primary key) of 1. I can successful query the API by going to http://localhost:8000/api/v1/case/1/
I receive a valid JSON response (Trampe is a rabbit)
{
"id": 1,
"total_points": 5000,
"passing_points": 3700,
"budget": 5000,
"description": "Saving Trampe from Trauma",
"name": "Trampe",
"signalment": "8yr, intact male, mixed breed.",
"problem": "Respiratory difficulty",
"image": {
"id": 1,
"file": "http://localhost:8000/media/images/trampe.jpg",
"description": "A lovely picture of Trampe"
},
My API requires authentication, and as such I am providing authentication in my test case.
class CaseTests(APITestCase):
def test_status_code(self):
"""
ensure that case/1 returns 200 OK
"""
# Create a test user
test_user = User(username='jim', password='monkey123', email='jim#jim.com')
test_user.save()
# build a factory and get our user Jim
factory = APIRequestFactory()
user = User.objects.get(username='jim')
# Get our view to test and the url, too
view = EntireCaseView.as_view()
url = reverse('case', kwargs={'pk': '1'})
print(url.__str__())
# Make an authenticated request to the view...
request = factory.get(url)
print(request.get_full_path())
force_authenticate(request, user=user)
response = view(request, "1")
print(response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Of interest here (at least to me) are the lines
url = reverse('case', kwargs={'pk': '1'})
and
response = view(request, "1")
If I leave out either the kwargs argument in url =r everse('case', kwargs={'pk': '1'}) or the "1" in response = view(request, "1") I will receive an error saying that the get() method requires 2 positional arguments but only given.
Here is the signature of the get() method in my view.
class EntireCaseView(APIView):
def get(self, request, pk):
If I run my test, Django reports that it fails because of a 404.
self.assertEqual(response.status_code, status.HTTP_200_OK)
AssertionError: 404 != 200
What I am trying to work out is why this is the case. Printing print(url.__str__()) outputs /api/v1/case/1/ as does print(request.get_full_path())
So in summary, I am trying to understand why I'm receiving this 404, and ultimately, how I can test this, and other endpoints.
Any and all help is appreciated.
Cheers,
C

How to return customized JSON response for an error in django-graphene?

I am learning to use django-graphene for graphql purpose.
For mutation, all I know is it would return error message of its own.
Let's say if I have a token field, and checked if token field is bad, I only know how to return None which would give the front end a query result of null instead of a customized json response for status and error
I have these codes
class ProductType(DjangoObjectType):
class Meta:
model = Product
filter_fields = {'description': ['icontains']}
interfaces = (graphene.relay.Node,)
class ProductInput(graphene.InputObjectType):
token = graphene.String()
title = graphene.String()
barcode = graphene.String(required=True)
class CreateProduct(graphene.Mutation):
class Arguments:
product_input = ProductInput()
product = graphene.Field(ProductType)
def mutate(self, info, product_input=None):
if not product_input.get('token'):
return None # instead of return None, I would like to return error code, status with message
product = Product.objects.create(barcode=product_input.barcode, title=product_input.title)
return CreateProduct(product=product)
class ProductMutation(graphene.ObjectType):
create_product = CreateProduct.Field()
Thanks in advance
Instead of return None you can throw an exception. The exception will be handled by graphene and passed on as an error.
For example, raise Exception('Error 123456') will result in a response something like
{
"errors": [
{
"message": "Error 123456'",
"locations": [
{
"line": 1,
"column": 3
}
]
}
],
"data": {
"product": null
}
}
The presence of errors in the output JSON can trigger the frontend error handling.
Please note that in general, any exceptions passed to graphql will be visible to the outside world, so it's worth considering the security ramifications of all of your graphene queries and mutation exceptions.