Coverage for /Users/davegaeddert/Development/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(

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

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

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

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