Skip to content

Commit b3c620c

Browse files
committed
Refactor exception_handler to use formatter over handlers
1 parent 9f2cf2c commit b3c620c

File tree

2 files changed

+90
-37
lines changed

2 files changed

+90
-37
lines changed
Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,125 @@
11
import logging
2+
from typing import Dict, Union
23

34
from django.core.exceptions import (
45
PermissionDenied,
56
ValidationError as DjangoValidationError,
67
)
78
from django.http import Http404
8-
from rest_framework import exceptions, status
9+
from rest_framework import exceptions
910
from rest_framework.response import Response
1011
from rest_framework.serializers import as_serializer_error
1112
from rest_framework.views import set_rollback
1213

13-
from .handlers import exc_detail_handler, is_exc_detail_same_as_default_detail
14-
from .settings import api_settings
14+
from drf_simple_api_errors import formatter
15+
from drf_simple_api_errors.exceptions import ServerError
16+
from drf_simple_api_errors.settings import api_settings
17+
from drf_simple_api_errors.types import ExceptionHandlerContext
1518

1619
logger = logging.getLogger(__name__)
1720

1821

19-
def exception_handler(exc, context):
22+
def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Response:
2023
"""
21-
Returns the response that should be used for any given exception.
24+
Custom exception handler for DRF.
2225
23-
By default this handles any REST framework `APIException`, and also
24-
Django's built-in `ValidationError`, `Http404` and `PermissionDenied` exceptions.
26+
This function handles exceptions and formats them into a structured API response,
27+
including Django exceptions. It also applies any extra handlers defined in the
28+
settings.
2529
26-
Any unhandled exceptions will log the exception message, and
27-
will cause a 500 error response.
30+
The function will not handle exceptions that are not instances of or
31+
can be converted to DRF `APIException`.
32+
33+
Args:
34+
exc (Exception): The exception raised.
35+
context (ExceptionHandlerContext): The context of the exception.
36+
37+
Returns:
38+
Response: The formatted API response.
2839
"""
2940

30-
if isinstance(exc, DjangoValidationError):
31-
exc = exceptions.ValidationError(as_serializer_error(exc))
41+
# This allows for custom exception handling logic.
42+
# If other kinds of exceptions are raised and should be handled,
43+
# they can be added to the EXTRA_HANDLERS setting.
44+
_apply_extra_handlers(exc)
3245

33-
if isinstance(exc, Http404):
34-
exc = exceptions.NotFound()
46+
# If the exception is not an instance of APIException, we can try to convert it
47+
# to DRF APIException if it's a Django exception.
48+
exc = _convert_django_exc_to_drf_api_exc(exc)
49+
# If the exception is still not an instance of APIException, thus could be
50+
# converted to one, we cannot handle it.
51+
# This will result in a 500 error response without any detail.
52+
# This is because it's not good practice to expose the details of
53+
# unhandled exceptions to the client.
54+
if not isinstance(exc, exceptions.APIException):
55+
logger.debug("Server error", exc_info=True)
56+
return ServerError
3557

36-
if isinstance(exc, PermissionDenied):
37-
exc = exceptions.PermissionDenied()
58+
# Get the API response headers from the exception.
59+
headers = _get_response_headers(exc)
60+
# Get the API response data from the exception.
61+
# If the exception is an instance of APIException, we can handle it and
62+
# will format it to a structured API response data.
63+
data = formatter.format_exc(exc)
64+
# Set the rollback flag to True, if the transaction is atomic.
65+
set_rollback()
66+
# Finally, return the API response \(◕ ◡ ◕\)
67+
return Response(data, status=exc.status_code, headers=headers)
3868

69+
70+
def _apply_extra_handlers(exc: Exception):
71+
"""
72+
Apply any extra exception handlers defined in the settings.
73+
74+
Args:
75+
exc (Exception): The exception to handle.
76+
"""
3977
extra_handlers = api_settings.EXTRA_HANDLERS
4078
if extra_handlers:
4179
for handler in extra_handlers:
4280
handler(exc)
4381

44-
# unhandled exceptions, which should raise a 500 error and log the exception
45-
if not isinstance(exc, exceptions.APIException):
46-
logger.exception(exc)
47-
data = {"title": "Server error."}
48-
return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
4982

50-
# from DRF
83+
def _convert_django_exc_to_drf_api_exc(
84+
exc: Exception,
85+
) -> Union[exceptions.APIException, Exception]:
86+
"""
87+
Convert Django exceptions to DRF APIException, if possible.
88+
89+
Args:
90+
exc (Exception): The exception to convert.
91+
92+
Returns:
93+
exceptions.APIException | Exception: The converted exception or the original.
94+
"""
95+
if isinstance(exc, DjangoValidationError):
96+
return exceptions.ValidationError(as_serializer_error(exc))
97+
98+
if isinstance(exc, Http404):
99+
return exceptions.NotFound()
100+
101+
if isinstance(exc, PermissionDenied):
102+
return exceptions.PermissionDenied()
103+
104+
return exc
105+
106+
107+
def _get_response_headers(exc: exceptions.APIException) -> Dict:
108+
"""
109+
Get the response headers for the given exception.
110+
111+
Args:
112+
exc (exceptions.APIException): The exception to get headers for.
113+
114+
Returns:
115+
dict: A dictionary containing the response headers.
116+
"""
117+
# This is from DRF's default exception handler.
51118
# https://github.com/encode/django-rest-framework/blob/48a21aa0eb3a95d32456c2a927eff9552a04231e/rest_framework/views.py#L87-L91
52119
headers = {}
53120
if getattr(exc, "auth_header", None):
54121
headers["WWW-Authenticate"] = exc.auth_header
55122
if getattr(exc, "wait", None):
56123
headers["Retry-After"] = "%d" % exc.wait
57124

58-
data = {}
59-
if isinstance(exc.detail, (list, dict)) and isinstance(
60-
exc, exceptions.ValidationError
61-
):
62-
data["title"] = "Validation error."
63-
exc_detail_handler(data, exc.detail)
64-
else:
65-
data["title"] = exc.default_detail
66-
if not is_exc_detail_same_as_default_detail(exc):
67-
exc_detail_handler(
68-
data, [exc.detail] if isinstance(exc.detail, str) else exc.detail
69-
)
70-
71-
set_rollback()
72-
return Response(data, status=exc.status_code, headers=headers)
125+
return headers

drf_simple_api_errors/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22

3-
from .settings import api_settings
3+
from drf_simple_api_errors.settings import api_settings
44

55

66
def camelize(field: str) -> str:

0 commit comments

Comments
 (0)