Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/internal/handlers/exception.py: 36%
59 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
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(f"plain.security.{exc.__class__.__name__}")
89 security_logger.error(
90 str(exc),
91 exc_info=exc,
92 extra={"status_code": 400, "request": request},
93 )
94 response = get_exception_response(request, 400)
96 else:
97 signals.got_request_exception.send(sender=None, request=request)
98 response = get_exception_response(request, 500)
99 log_response(
100 "%s: %s",
101 response.reason_phrase,
102 request.path,
103 response=response,
104 request=request,
105 exception=exc,
106 )
108 # Force a TemplateResponse to be rendered.
109 if not getattr(response, "is_rendered", True) and callable(
110 getattr(response, "render", None)
111 ):
112 response = response.render()
114 return response
117def get_exception_response(request, status_code):
118 try:
119 return get_error_view(status_code)(request)
120 except Exception:
121 signals.got_request_exception.send(sender=None, request=request)
122 return handle_uncaught_exception()
125def handle_uncaught_exception():
126 """
127 Processing for any otherwise uncaught exceptions (those that will
128 generate HTTP 500 responses).
129 """
130 return ResponseServerError()
133def get_error_view(status_code):
134 views_by_status = settings.HTTP_ERROR_VIEWS
135 if status_code in views_by_status:
136 view = views_by_status[status_code]
137 if isinstance(view, str):
138 # Import the view if it's a string
139 view = import_string(view)
140 return view.as_view()
142 # Create a standard view for any other status code
143 return ErrorView.as_view(status_code=status_code)