mirror of
				https://github.com/HackSoftware/Django-Styleguide.git
				synced 2025-11-04 09:27:30 +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