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