mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2025-02-16 19:41:00 +03:00
Add section about error formatting
- Include a bunch of things for `utils.py`
This commit is contained in:
parent
55e135436d
commit
f2e237765c
87
README.md
87
README.md
|
@ -30,6 +30,7 @@ Expect often updates as we discuss & decide upon different things.
|
||||||
- [Exception Handling](#exception-handling)
|
- [Exception Handling](#exception-handling)
|
||||||
* [Raising Exceptions in Services / Selectors](#raising-exceptions-in-services--selectors)
|
* [Raising Exceptions in Services / Selectors](#raising-exceptions-in-services--selectors)
|
||||||
* [Handle Exceptions in APIs](#handle-exceptions-in-apis)
|
* [Handle Exceptions in APIs](#handle-exceptions-in-apis)
|
||||||
|
* [Error formatting](#error-formatting)
|
||||||
- [Testing](#testing-1)
|
- [Testing](#testing-1)
|
||||||
* [Naming conventions](#naming-conventions)
|
* [Naming conventions](#naming-conventions)
|
||||||
* [Example](#example)
|
* [Example](#example)
|
||||||
|
@ -646,6 +647,92 @@ class CourseCreateApi(
|
||||||
|
|
||||||
All of code above can be found in `utils.py` in this repository.
|
All of code above can be found in `utils.py` in this repository.
|
||||||
|
|
||||||
|
### Error formatting
|
||||||
|
|
||||||
|
Next step is to generalize the format of the errors we get from our APIs. This will ease the process of displaying errors to the end user, via JavaScript.
|
||||||
|
|
||||||
|
If we have a standard serializer and there is error with one of the fields, the message we get by default looks like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"url": [
|
||||||
|
"This field is required."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we have a validation error with just a message (`raise ValidationError('Something is wrong.')`), it will look like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
"some error"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Anoter error format may look like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"detail": "Method \"GET\" not allowed."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Those are 3 different ways of formatting for our errors. Luckily, DRF provides a way for us to provide our own custom exception handling, where we can plug in with the formatting we want: <https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling>
|
||||||
|
|
||||||
|
In our projects, we follow this general way of formatting errors:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Error message",
|
||||||
|
"code": "Some code",
|
||||||
|
"field": "field_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Error message",
|
||||||
|
"code": "Some code",
|
||||||
|
"field": "nested.field_name"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we raise a `ValidationError`, then field is optional.
|
||||||
|
|
||||||
|
In order to acheive that, we implement a custom exception handler:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.views import exception_handler
|
||||||
|
|
||||||
|
|
||||||
|
def exception_errors_format_handler(exc, context):
|
||||||
|
response = exception_handler(exc, context)
|
||||||
|
|
||||||
|
# If unexpected error occurs (server error, etc.)
|
||||||
|
if response is None:
|
||||||
|
return response
|
||||||
|
|
||||||
|
formatter = ErrorsFormatter(exc)
|
||||||
|
|
||||||
|
response.data = formatter()
|
||||||
|
|
||||||
|
return response
|
||||||
|
```
|
||||||
|
|
||||||
|
which needs to be to the `REST_FRAMEWORK` project settings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'EXCEPTION_HANDLER': 'project.app.handlers.exception_errors_format_handler',
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The magic happens in the `ErrorsFormatter` class. The implementation of that class can be found in the `utils.py` file, located in that repo.
|
||||||
|
|
||||||
|
Combining `ApiErrorsMixin`, the custom exception handler & the errors formatter class, we can have predictable behavior in our APIs, when it comes to errors.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
In our Django projects, we split our tests depending on the type of code they represent.
|
In our Django projects, we split our tests depending on the type of code they represent.
|
||||||
|
|
148
utils.py
148
utils.py
|
@ -1,6 +1,10 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework import exceptions as rest_exceptions
|
from rest_framework import exceptions as rest_exceptions
|
||||||
|
|
||||||
|
from rest_framework.views import exception_handler
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework import exceptions
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +43,7 @@ def get_error_message(exc):
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
|
|
||||||
class ExceptionHandlerMixin:
|
class ApiErrorsMixin:
|
||||||
"""
|
"""
|
||||||
Mixin that transforms Django and Python exceptions into rest_framework ones.
|
Mixin that transforms Django and Python exceptions into rest_framework ones.
|
||||||
without the mixin, they return 500 status code which is not desired.
|
without the mixin, they return 500 status code which is not desired.
|
||||||
|
@ -58,3 +62,145 @@ class ExceptionHandlerMixin:
|
||||||
return super().handle_exception(drf_exception)
|
return super().handle_exception(drf_exception)
|
||||||
|
|
||||||
return super().handle_exception(exc)
|
return super().handle_exception(exc)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorsFormatter:
|
||||||
|
"""
|
||||||
|
The current formatter gets invalid serializer errors,
|
||||||
|
uses DRF standart for code and messaging
|
||||||
|
and then parses it to the following format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Error message",
|
||||||
|
"code": "Some code",
|
||||||
|
"field": "field_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "Error message",
|
||||||
|
"code": "Some code",
|
||||||
|
"field": "nested.field_name"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
FIELD = 'field'
|
||||||
|
MESSAGE = 'message'
|
||||||
|
CODE = 'code'
|
||||||
|
ERRORS = 'errors'
|
||||||
|
|
||||||
|
def __init__(self, exception):
|
||||||
|
self.exception = exception
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
if hasattr(self.exception, 'get_full_details'):
|
||||||
|
formatted_errors = self._get_response_json_from_drf_errors(
|
||||||
|
serializer_errors=self.exception.get_full_details()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
formatted_errors = self._get_response_json_from_error_message(message=str(self.exception))
|
||||||
|
|
||||||
|
return formatted_errors
|
||||||
|
|
||||||
|
def _get_response_json_from_drf_errors(self, serializer_errors=None):
|
||||||
|
if serializer_errors is None:
|
||||||
|
serializer_errors = {}
|
||||||
|
|
||||||
|
if type(serializer_errors) is list:
|
||||||
|
serializer_errors = {
|
||||||
|
api_settings.NON_FIELD_ERRORS_KEY: serializer_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
list_of_errors = self._get_list_of_errors(errors_dict=serializer_errors)
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
self.ERRORS: list_of_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def _get_response_json_from_error_message(self, *, message='', field=None, code='error'):
|
||||||
|
response_data = {
|
||||||
|
self.ERRORS: [
|
||||||
|
{
|
||||||
|
self.MESSAGE: message,
|
||||||
|
self.CODE: code
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if field:
|
||||||
|
response_data[self.ERRORS][self.FIELD] = field
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def _unpack(self, obj):
|
||||||
|
if type(obj) is list and len(obj) == 1:
|
||||||
|
return obj[0]
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def _get_list_of_errors(self, field_path='', errors_dict=None):
|
||||||
|
"""
|
||||||
|
Error_dict is in the following format:
|
||||||
|
{
|
||||||
|
'field1': {
|
||||||
|
'message': 'some message..'
|
||||||
|
'code' 'some code...'
|
||||||
|
},
|
||||||
|
'field2: ...'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if errors_dict is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
message_value = errors_dict.get(self.MESSAGE, None)
|
||||||
|
|
||||||
|
# Note: If 'message' is name of a field we don't want to stop the recursion here!
|
||||||
|
if message_value is not None and\
|
||||||
|
(type(message_value) in {str, exceptions.ErrorDetail}):
|
||||||
|
if field_path:
|
||||||
|
errors_dict[self.FIELD] = field_path
|
||||||
|
return [errors_dict]
|
||||||
|
|
||||||
|
errors_list = []
|
||||||
|
for key, value in errors_dict.items():
|
||||||
|
new_field_path = '{0}.{1}'.format(field_path, key) if field_path else key
|
||||||
|
key_is_non_field_errors = key == api_settings.NON_FIELD_ERRORS_KEY
|
||||||
|
|
||||||
|
if type(value) is list:
|
||||||
|
current_level_error_list = []
|
||||||
|
new_value = value
|
||||||
|
|
||||||
|
for error in new_value:
|
||||||
|
# if the type of field_error is list we need to unpack it
|
||||||
|
field_error = self._unpack(error)
|
||||||
|
|
||||||
|
if not key_is_non_field_errors:
|
||||||
|
field_error[self.FIELD] = new_field_path
|
||||||
|
|
||||||
|
current_level_error_list.append(field_error)
|
||||||
|
else:
|
||||||
|
path = field_path if key_is_non_field_errors else new_field_path
|
||||||
|
|
||||||
|
current_level_error_list = self._get_list_of_errors(field_path=path, errors_dict=value)
|
||||||
|
|
||||||
|
errors_list += current_level_error_list
|
||||||
|
|
||||||
|
return errors_list
|
||||||
|
|
||||||
|
|
||||||
|
def exception_errors_format_handler(exc, context):
|
||||||
|
response = exception_handler(exc, context)
|
||||||
|
|
||||||
|
# If unexpected error occurs (server error, etc.)
|
||||||
|
if response is None:
|
||||||
|
return response
|
||||||
|
|
||||||
|
formatter = ErrorsFormatter(exc)
|
||||||
|
|
||||||
|
response.data = formatter()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
Loading…
Reference in New Issue
Block a user