-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathhandler.py
More file actions
140 lines (123 loc) · 5.33 KB
/
handler.py
File metadata and controls
140 lines (123 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import sys
from typing import Optional
import django
from django.conf import settings
from django.core import signals
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.log import log_response
from rest_framework import exceptions
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.status import is_server_error
from rest_framework.views import set_rollback
from .formatter import ExceptionFormatter
from .settings import package_settings
from .types import ExceptionHandlerContext
def exception_handler(
exc: Exception, context: ExceptionHandlerContext
) -> Optional[Response]:
exception_handler_class = package_settings.EXCEPTION_HANDLER_CLASS
msg = "`EXCEPTION_HANDLER_CLASS` should be a subclass of ExceptionHandler."
assert issubclass(exception_handler_class, ExceptionHandler), msg
return exception_handler_class(exc, context).run()
class ExceptionHandler:
def __init__(self, exc: Exception, context: ExceptionHandlerContext):
self.exc = exc
self.context = context
def run(self) -> Optional[Response]:
"""entrypoint for handling an exception"""
exc = self.convert_known_exceptions(self.exc)
if self.should_not_handle(exc):
return None
exc = self.convert_unhandled_exceptions(exc)
data = self.format_exception(exc)
self.set_rollback()
response = self.get_response(exc, data)
self.report_exception(exc, response)
return response
def convert_known_exceptions(self, exc: Exception) -> Exception:
"""
By default, Django's built-in `Http404` and `PermissionDenied` are converted
to their DRF equivalent.
"""
if isinstance(exc, Http404):
return exceptions.NotFound()
elif isinstance(exc, PermissionDenied):
return exceptions.PermissionDenied()
else:
return exc
def should_not_handle(self, exc: Exception) -> bool:
"""
By default, don't handle non-DRF errors in DEBUG mode. That's because
handling the exception means the developer will not see the exception
traceback.
"""
return (
getattr(settings, "DEBUG", False)
and not package_settings.ENABLE_IN_DEBUG_FOR_UNHANDLED_EXCEPTIONS
and not isinstance(exc, exceptions.APIException)
)
def convert_unhandled_exceptions(self, exc: Exception) -> exceptions.APIException:
"""
Any non-DRF unhandled exception is converted to an APIException which
has a 500 status code.
"""
if not isinstance(exc, exceptions.APIException):
return exceptions.APIException(detail=str(exc))
else:
return exc
def format_exception(self, exc: exceptions.APIException) -> dict:
exception_formatter_class = package_settings.EXCEPTION_FORMATTER_CLASS
msg = "`EXCEPTION_FORMATTER_CLASS` should be a subclass of ExceptionFormatter."
assert issubclass(exception_formatter_class, ExceptionFormatter), msg
return exception_formatter_class(exc, self.context, self.exc).run()
def set_rollback(self) -> None:
set_rollback()
def get_response(self, exc: exceptions.APIException, data: dict) -> Response:
headers = self.get_headers(exc)
return Response(data, status=exc.status_code, headers=headers)
def get_headers(self, exc: exceptions.APIException) -> dict:
headers = {}
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["Retry-After"] = "%d" % exc.wait
return headers
def report_exception(
self, exc: exceptions.APIException, response: Response
) -> None:
"""
Normally, when an exception is unhandled (non-DRF exception), DRF delegates
handling it to Django. Django, then, takes care of returning the appropriate
response. That is done in: django.core.handlers.exception.convert_exception_to_response
However, this package handles all exceptions. So, to stay in line with Django's
default behavior, the got_request_exception signal is sent and the response is
also logged. Sending the signal should allow error monitoring tools (like Sentry)
to work as usual (error is captured and sent to their servers).
"""
if is_server_error(exc.status_code):
try:
drf_request: Request = self.context["request"]
request = drf_request._request
except AttributeError:
request = None
signals.got_request_exception.send(sender=None, request=request)
if django.VERSION < (4, 1):
log_response(
"%s: %s",
exc.detail,
getattr(request, "path", ""),
response=response,
request=request,
exc_info=sys.exc_info(),
)
else:
log_response(
"%s: %s",
exc.detail,
getattr(request, "path", ""),
response=response,
request=request,
exception=self.exc,
)