Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/internal/handlers/exception.py: 36%

59 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:04 -0500

1import logging 

2from functools import wraps 

3 

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 

19 

20 

21def convert_exception_to_response(get_response): 

22 """ 

23 Wrap the given get_response callable in exception-to-response conversion. 

24 

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. 

29 

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 """ 

34 

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 

42 

43 return inner 

44 

45 

46def response_for_exception(request, exc): 

47 if isinstance(exc, Http404): 

48 response = get_exception_response(request, 404) 

49 

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 ) 

59 

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 ) 

69 

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() 

85 

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) 

97 

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 ) 

109 

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() 

115 

116 return response 

117 

118 

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() 

125 

126 

127def handle_uncaught_exception(): 

128 """ 

129 Processing for any otherwise uncaught exceptions (those that will 

130 generate HTTP 500 responses). 

131 """ 

132 return ResponseServerError() 

133 

134 

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() 

143 

144 # Create a standard view for any other status code 

145 return ErrorView.as_view(status_code=status_code)