Coverage for postrfp/web/exception.py: 98%

96 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-22 21:34 +0000

1import json 

2import logging 

3import traceback 

4from pprint import pformat 

5import functools 

6 

7import webob 

8import webob.exc 

9from sqlalchemy.orm.exc import NoResultFound 

10 

11from postrfp.shared.exceptions import AuthorizationFailure 

12from postrfp.shared.constants import MIME_TYPE_VALUES 

13from postrfp.web.request import HttpRequest 

14from postrfp.templates import get_template 

15from postrfp.web.suxint import RoutingError 

16from postrfp.shared.exceptions import NotLoggedIn 

17from postrfp.model.exc import ( 

18 BusinessRuleViolation, 

19 CosmeticQuestionEditViolation, 

20 QuestionnaireStructureException, 

21 ValidationFailure, 

22 DuplicateDataProvided, 

23 DuplicateQuestionDefinition, 

24) 

25 

26log = logging.getLogger(__name__) 

27 

28 

29def render_error( 

30 request: HttpRequest, 

31 response_class, 

32 message=None, 

33 title="Server Error", 

34 errors=None, 

35): 

36 try: 

37 if request.prefers_json: 

38 # Assume a browswer ajax call - prepare response that our js client understands 

39 err_doc = dict(error=title, description=message) 

40 if errors is not None: 

41 if hasattr(errors, "as_dict"): 

42 err_doc["errors"] = errors.as_dict() 

43 elif isinstance(errors, (list, dict)): 

44 err_doc["errors"] = errors 

45 else: 

46 err_doc["errors"] = str(errors) 

47 res_js = json.dumps(err_doc) 

48 return response_class( 

49 body=res_js, charset="utf-8", content_type=str("application/json") 

50 ) 

51 

52 elif request.accept.accepts_html: 

53 # Attempt to produce a reasonable looking error message 

54 template = get_template("error.html") 

55 html_output = template.render( 

56 message=message, title=title, request=request, errors=errors 

57 ) 

58 return response_class(body=html_output) 

59 

60 elif request.accept.acceptable_offers(offers=MIME_TYPE_VALUES): 

61 return response_class(body=f"Error: {message}") 

62 

63 else: 

64 raise Exception( 

65 f"Unable to determine appropriate response type for {request.accept}" 

66 ) 

67 

68 except Exception as exc: 

69 log_exception(request, exc, logging.ERROR) 

70 m = "An unhandled error occurred. Our team is aware of the problem and is addressing it." 

71 return webob.exc.HTTPError(m) 

72 

73 

74def log_exception(request, exception, severity): 

75 """Format and log exception""" 

76 

77 stack_trace = traceback.format_exc() # Note this formats the 'current' exception 

78 environ = pformat(request.environ) 

79 

80 log_msg = "%s: %s\n\n%s %s\n\n%s\n%s" % ( 

81 exception.__class__.__name__, 

82 str(exception), 

83 request.method, 

84 request.path_url, 

85 stack_trace, 

86 environ, 

87 ) 

88 

89 log.log(severity, log_msg) 

90 

91 

92def ex_message(ex, default="No further details"): 

93 if hasattr(ex, "message"): 

94 return ex.message 

95 if hasattr(ex, "detail"): 

96 return ex.detail 

97 return default 

98 

99 

100def resolve_exception(req: HttpRequest, e: Exception): 

101 """Map Exceptions to HTTP Responses and write Log message""" 

102 

103 # By default log exceptions with severity of ERROR 

104 severity = logging.INFO 

105 if isinstance(e, webob.exc.HTTPRedirection): 

106 return e 

107 

108 error = functools.partial(render_error, req) 

109 

110 if isinstance(e, (NotLoggedIn, webob.exc.HTTPUnauthorized)): 

111 response = error( 

112 webob.exc.HTTPUnauthorized, message=ex_message(e), title="Not Logged In" 

113 ) 

114 

115 # Format the exception for display to the user 

116 elif isinstance(e, (NoResultFound, RoutingError, webob.exc.HTTPNotFound)): 

117 response = error( 

118 webob.exc.HTTPNotFound, 

119 "The resource requested is not present on the server", 

120 title="Not Found", 

121 ) 

122 

123 elif isinstance(e, (AuthorizationFailure, webob.exc.HTTPForbidden)): 

124 severity = logging.WARN 

125 msg = "The action you have attempted is forbidden: %s" % ex_message(e) 

126 response = error( 

127 webob.exc.HTTPForbidden, 

128 message=msg, 

129 title="Forbidden Action", 

130 errors=getattr(e, "errors", None), 

131 ) 

132 

133 elif isinstance(e, ValidationFailure): 

134 msg = "The request could not be processed due to invalid data provided." 

135 response = error( 

136 webob.exc.HTTPBadRequest, 

137 message=msg, 

138 title="Invalid Information Submitted", 

139 errors=e.errors_list, 

140 ) 

141 

142 elif isinstance(e, ValueError): 

143 msg = "The request could not be processed due to invalid data provided." 

144 response = error( 

145 webob.exc.HTTPBadRequest, 

146 message=msg, 

147 title="Invalid Information Submitted", 

148 errors=[str(e)], 

149 ) 

150 

151 elif isinstance(e, DuplicateDataProvided): 

152 msg = ex_message(e, default="Duplicate Data provided") 

153 response = error(webob.exc.HTTPConflict, message=msg, title="Data Conflict") 

154 

155 elif isinstance(e, CosmeticQuestionEditViolation): 

156 msg = ex_message( 

157 e, default="Cannot delete question elements that have related answers" 

158 ) 

159 response = error( 

160 webob.exc.HTTPConflict, message=msg, title="Data Integrity Violation" 

161 ) 

162 

163 elif isinstance(e, webob.exc.HTTPBadRequest): 

164 msg = ex_message( 

165 e, default="Bad Request - probably an outdated or corrupted link" 

166 ) 

167 response = error( 

168 webob.exc.HTTPBadRequest, 

169 message=msg, 

170 title="Bad Request - possibly an outdate or corrupt link", 

171 ) 

172 

173 elif isinstance(e, QuestionnaireStructureException): 

174 msg = ex_message(e, default="Invalid questionnaire structure") 

175 response = error( 

176 webob.exc.HTTPBadRequest, 

177 message=msg, 

178 title="Questionnaire Structure Violation", 

179 ) 

180 

181 elif isinstance(e, BusinessRuleViolation): 

182 msg = ex_message(e, default="Illegal Action: %s" % e.message) 

183 response = error(webob.exc.HTTPBadRequest, message=msg, title="Illegal Action") 

184 

185 elif isinstance(e, DuplicateQuestionDefinition): 

186 msg = ex_message(e, default="Question has already been shared to this project") 

187 response = error( 

188 webob.exc.HTTPBadRequest, 

189 message=msg, 

190 title="Bad Request - Section Data Integrity Violation", 

191 ) 

192 

193 elif isinstance(e, webob.exc.HTTPError): 

194 response = error(type(e), e) 

195 

196 else: 

197 severity = logging.ERROR 

198 msg = "An unexpected error occurred processing your request." 

199 msg += " Our engineers have been informed so there is no need for action on your part." 

200 msg += " It may help to reload the page." 

201 response = error(webob.exc.HTTPServerError, msg) 

202 

203 log_exception(req, e, severity) 

204 

205 return response