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

95 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-24 10:52 +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 rfpy.web.mime import MIME_TYPE_VALUES 

12from rfpy.web.request import HttpRequest 

13from rfpy.templates import get_template 

14from rfpy.suxint import RoutingError 

15from rfpy.auth import AuthorizationFailure, NotLoggedIn 

16from rfpy.model.exc import ( 

17 BusinessRuleViolation, 

18 CosmeticQuestionEditViolation, 

19 QuestionnaireStructureException, 

20 ValidationFailure, 

21 DuplicateDataProvided, 

22 DuplicateQuestionDefinition, 

23) 

24 

25log = logging.getLogger(__name__) 

26 

27 

28def render_error( 

29 request: HttpRequest, 

30 response_class, 

31 message=None, 

32 title="Server Error", 

33 errors=None, 

34): 

35 try: 

36 if request.prefers_json: 

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

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

39 if errors is not None: 

40 if hasattr(errors, "as_dict"): 

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

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

43 err_doc["errors"] = errors 

44 else: 

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

46 res_js = json.dumps(err_doc) 

47 return response_class( 

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

49 ) 

50 

51 elif request.accept.accepts_html: 

52 # Attempt to produce a reasonable looking error message 

53 template = get_template("error.html") 

54 html_output = template.render( 

55 message=message, title=title, request=request, errors=errors 

56 ) 

57 return response_class(body=html_output) 

58 

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

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

61 

62 else: 

63 raise Exception( 

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

65 ) 

66 

67 except Exception as exc: 

68 log_exception(request, exc, logging.ERROR) 

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

70 return webob.exc.HTTPError(m) 

71 

72 

73def log_exception(request, exception, severity): 

74 """Format and log exception""" 

75 

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

77 environ = pformat(request.environ) 

78 

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

80 exception.__class__.__name__, 

81 str(exception), 

82 request.method, 

83 request.path_url, 

84 stack_trace, 

85 environ, 

86 ) 

87 

88 log.log(severity, log_msg) 

89 

90 

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

92 if hasattr(ex, "message"): 

93 return ex.message 

94 if hasattr(ex, "detail"): 

95 return ex.detail 

96 return default 

97 

98 

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

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

101 

102 # By default log exceptions with severity of ERROR 

103 severity = logging.INFO 

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

105 return e 

106 

107 error = functools.partial(render_error, req) 

108 

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

110 response = error( 

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

112 ) 

113 

114 # Format the exception for display to the user 

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

116 response = error( 

117 webob.exc.HTTPNotFound, 

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

119 title="Not Found", 

120 ) 

121 

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

123 severity = logging.WARN 

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

125 response = error( 

126 webob.exc.HTTPForbidden, 

127 message=msg, 

128 title="Forbidden Action", 

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

130 ) 

131 

132 elif isinstance(e, ValidationFailure): 

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

134 response = error( 

135 webob.exc.HTTPBadRequest, 

136 message=msg, 

137 title="Invalid Information Submitted", 

138 errors=e.errors_list, 

139 ) 

140 

141 elif isinstance(e, ValueError): 

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

143 response = error( 

144 webob.exc.HTTPBadRequest, 

145 message=msg, 

146 title="Invalid Information Submitted", 

147 errors=[str(e)], 

148 ) 

149 

150 elif isinstance(e, DuplicateDataProvided): 

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

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

153 

154 elif isinstance(e, CosmeticQuestionEditViolation): 

155 msg = ex_message( 

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

157 ) 

158 response = error( 

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

160 ) 

161 

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

163 msg = ex_message( 

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

165 ) 

166 response = error( 

167 webob.exc.HTTPBadRequest, 

168 message=msg, 

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

170 ) 

171 

172 elif isinstance(e, QuestionnaireStructureException): 

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

174 response = error( 

175 webob.exc.HTTPBadRequest, 

176 message=msg, 

177 title="Questionnaire Structure Violation", 

178 ) 

179 

180 elif isinstance(e, BusinessRuleViolation): 

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

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

183 

184 elif isinstance(e, DuplicateQuestionDefinition): 

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

186 response = error( 

187 webob.exc.HTTPBadRequest, 

188 message=msg, 

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

190 ) 

191 

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

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

194 

195 else: 

196 severity = logging.ERROR 

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

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

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

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

201 

202 log_exception(req, e, severity) 

203 

204 return response