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
« 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
7import webob
8import webob.exc
9from sqlalchemy.orm.exc import NoResultFound
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)
25log = logging.getLogger(__name__)
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 )
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)
59 elif request.accept.acceptable_offers(offers=MIME_TYPE_VALUES):
60 return response_class(body=f"Error: {message}")
62 else:
63 raise Exception(
64 f"Unable to determine appropriate response type for {request.accept}"
65 )
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)
73def log_exception(request, exception, severity):
74 """Format and log exception"""
76 stack_trace = traceback.format_exc() # Note this formats the 'current' exception
77 environ = pformat(request.environ)
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 )
88 log.log(severity, log_msg)
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
99def resolve_exception(req: HttpRequest, e: Exception):
100 """Map Exceptions to HTTP Responses and write Log message"""
102 # By default log exceptions with severity of ERROR
103 severity = logging.INFO
104 if isinstance(e, webob.exc.HTTPRedirection):
105 return e
107 error = functools.partial(render_error, req)
109 if isinstance(e, (NotLoggedIn, webob.exc.HTTPUnauthorized)):
110 response = error(
111 webob.exc.HTTPUnauthorized, message=ex_message(e), title="Not Logged In"
112 )
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 )
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 )
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 )
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 )
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")
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 )
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 )
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 )
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")
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 )
192 elif isinstance(e, webob.exc.HTTPError):
193 response = error(type(e), e)
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)
202 log_exception(req, e, severity)
204 return response