Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/internal/handlers/exception.py: 54%

59 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

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

95 

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 ) 

107 

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

113 

114 return response 

115 

116 

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

123 

124 

125def handle_uncaught_exception(): 

126 """ 

127 Processing for any otherwise uncaught exceptions (those that will 

128 generate HTTP 500 responses). 

129 """ 

130 return ResponseServerError() 

131 

132 

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

141 

142 # Create a standard view for any other status code 

143 return ErrorView.as_view(status_code=status_code)