diff --git a/main/error_codes.py b/main/error_codes.py new file mode 100644 index 000000000..7d80e654c --- /dev/null +++ b/main/error_codes.py @@ -0,0 +1,3 @@ +TOKEN_INVALID = 4001 +NOT_AUTHENTICATED = 4011 +AUTHENTICATION_FAILED = 4012 diff --git a/main/errors.py b/main/errors.py new file mode 100644 index 000000000..a6564fc35 --- /dev/null +++ b/main/errors.py @@ -0,0 +1,22 @@ +from main import error_codes + + +error_code_map = { + 'not_authenticated': error_codes.NOT_AUTHENTICATED, + 'authentication_failed': error_codes.AUTHENTICATION_FAILED, +} + + +def map_error_codes(codes, default=None): + """ + Take in get_codes() value of drf exception + and return a corresponding internal error code. + """ + + if isinstance(codes, str): + return error_code_map.get(codes, default) + + if codes == {'non_field_errors': ['invalid']}: + return error_codes.TOKEN_INVALID + + return default diff --git a/main/exception_handler.py b/main/exception_handler.py index 5f093f524..fc8cfb9cf 100644 --- a/main/exception_handler.py +++ b/main/exception_handler.py @@ -1,11 +1,13 @@ import logging import sentry_sdk -import logging from rest_framework.views import exception_handler from rest_framework.response import Response -from rest_framework import status +from rest_framework import status, exceptions from django_read_only import DjangoReadOnlyError +from django.utils import timezone + +from main.errors import map_error_codes logger = logging.getLogger(__name__) @@ -56,4 +58,69 @@ def custom_exception_handler(exc, context): ) response = Response(response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # Empty the response body but keep the headers + response.data = {} + + # Timestamp of exception + response.data['timestamp'] = timezone.now() + + if isinstance(exc, (exceptions.NotAuthenticated,)): + response.status_code = status.HTTP_401_UNAUTHORIZED + elif hasattr(exc, 'status_code'): + response.status_code = exc.status_code + + if hasattr(exc, 'code'): + # If the raised exception defines a code, send it as + # internal error code + response.data['error_code'] = exc.code + elif hasattr(exc, 'get_codes'): + # Otherwise, try to map the exception.get_codes() value to an + # internal error code. + # If no internal code available, return http status code as + # internal error code by default. + response.data['error_code'] = map_error_codes( + exc.get_codes(), response.status_code) + else: + response.data['error_code'] = response.status_code + + # Error message can be defined by the exception as message + # or detail attributres + # Otherwise, it is simply the stringified exception. + + errors = None + user_error = None + + if hasattr(exc, 'message'): + errors = exc.message + elif hasattr(exc, 'detail'): + if isinstance(exc.detail, list): + errors = [str(error) for error in exc.detail] + else: + errors = exc.detail + elif hasattr(exc, 'default_detail'): + errors = exc.default_detail + elif response.status_code == 404: + errors = 'Resource not found' + else: + errors = str(exc) + user_error = standard_error_string + + if hasattr(exc, 'user_message'): + user_error = exc.user_message + + # Wrap up string error inside non-field-errors + if isinstance(errors, str): + errors = { + 'non_field_errors': [errors], + } + elif isinstance(errors, list) and all([isinstance(error, str) for error in errors]): + errors = { + 'non_field_errors': errors, + } + + if user_error: + errors['internal_non_field_errors'] = errors.get('non_field_errors') + errors['non_field_errors'] = [user_error] + + response.data['errors'] = errors return response