Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/internal/handlers/exception.py: 25%
59 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
1import logging
2from functools import wraps
4from plain import signals
5from plain.exceptions import (
6 BadRequest,
7 PermissionDenied,
8 RequestDataTooBig,
9 SuspiciousOperation,
10 TooManyFieldsSent,
11 TooManyFilesSent,
12)
13from plain.http import Http404, ResponseServerError
14from plain.http.multipartparser import MultiPartParserError
15from plain.logs import log_response
16from plain.runtime import settings
17from plain.utils.module_loading import import_string
18from plain.views.errors import ErrorView
21def convert_exception_to_response(get_response):
22 """
23 Wrap the given get_response callable in exception-to-response conversion.
25 All exceptions will be converted. All known 4xx exceptions (Http404,
26 PermissionDenied, MultiPartParserError, SuspiciousOperation) will be
27 converted to the appropriate response, and all other exceptions will be
28 converted to 500 responses.
30 This decorator is automatically applied to all middleware to ensure that
31 no middleware leaks an exception and that the next middleware in the stack
32 can rely on getting a response instead of an exception.
33 """
35 @wraps(get_response)
36 def inner(request):
37 try:
38 response = get_response(request)
39 except Exception as exc:
40 response = response_for_exception(request, exc)
41 return response
43 return inner
46def response_for_exception(request, exc):
47 if isinstance(exc, Http404):
48 response = get_exception_response(request, 404)
50 elif isinstance(exc, PermissionDenied):
51 response = get_exception_response(request, 403)
52 log_response(
53 "Forbidden (Permission denied): %s",
54 request.path,
55 response=response,
56 request=request,
57 exception=exc,
58 )
60 elif isinstance(exc, MultiPartParserError):
61 response = get_exception_response(request, 400)
62 log_response(
63 "Bad request (Unable to parse request body): %s",
64 request.path,
65 response=response,
66 request=request,
67 exception=exc,
68 )
70 elif isinstance(exc, BadRequest):
71 response = get_exception_response(request, 400)
72 log_response(
73 "%s: %s",
74 str(exc),
75 request.path,
76 response=response,
77 request=request,
78 exception=exc,
79 )
80 elif isinstance(exc, SuspiciousOperation):
81 if isinstance(exc, RequestDataTooBig | TooManyFieldsSent | TooManyFilesSent):
82 # POST data can't be accessed again, otherwise the original
83 # exception would be raised.
84 request._mark_post_parse_error()
86 # The request logger receives events for any problematic request
87 # The security logger receives events for all SuspiciousOperations
88 security_logger = logging.getLogger(
89 "plain.security.%s" % exc.__class__.__name__
90 )
91 security_logger.error(
92 str(exc),
93 exc_info=exc,
94 extra={"status_code": 400, "request": request},
95 )
96 response = get_exception_response(request, 400)
98 else:
99 signals.got_request_exception.send(sender=None, request=request)
100 response = get_exception_response(request, 500)
101 log_response(
102 "%s: %s",
103 response.reason_phrase,
104 request.path,
105 response=response,
106 request=request,
107 exception=exc,
108 )
110 # Force a TemplateResponse to be rendered.
111 if not getattr(response, "is_rendered", True) and callable(
112 getattr(response, "render", None)
113 ):
114 response = response.render()
116 return response
119def get_exception_response(request, status_code):
120 try:
121 return get_error_view(status_code)(request)
122 except Exception:
123 signals.got_request_exception.send(sender=None, request=request)
124 return handle_uncaught_exception()
127def handle_uncaught_exception():
128 """
129 Processing for any otherwise uncaught exceptions (those that will
130 generate HTTP 500 responses).
131 """
132 return ResponseServerError()
135def get_error_view(status_code):
136 views_by_status = settings.HTTP_ERROR_VIEWS
137 if status_code in views_by_status:
138 view = views_by_status[status_code]
139 if isinstance(view, str):
140 # Import the view if it's a string
141 view = import_string(view)
142 return view.as_view()
144 # Create a standard view for any other status code
145 return ErrorView.as_view(status_code=status_code)