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

1 import logging

2 from functools import wraps

3

4 from plain import signals

5 from plain.exceptions import (

6 BadRequest,

7 PermissionDenied,

8 RequestDataTooBig,

9 SuspiciousOperation,

10 TooManyFieldsSent,

11 TooManyFilesSent,

12)

13 from plain.http import Http404, ResponseServerError

14 from plain.http.multipartparser import MultiPartParserError

15 from plain.logs import log_response

16 from plain.runtime import settings

17 from plain.utils.module_loading import import_string

18 from plain.views.errors import ErrorView

19

20

21 def 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

46 def 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

117 def 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

125 def 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

133 def 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)