Dan b02731df17 Add some additional checks in the workflow service so we assure the data we are creating during validation can be serialized and deserialized just as it would be through the API. (Was hanging us up on dates)
Assure that if we generate a default value for a date in the task data, it is stored as an ISO String.

remove any unserializable data from the task_data when an error is encountered, rather than just dropping all the task_data. This case seems to happen a lot and it leaves us with nothing to go on.
2021-10-04 15:07:47 -04:00

124 lines
4.8 KiB
Python

import json
from SpiffWorkflow import WorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from flask import g
from werkzeug.exceptions import InternalServerError
from crc import ma, app
import sentry_sdk
class ApiError(Exception):
def __init__(self, code, message, status_code=400,
file_name="", task_id="", task_name="", tag="", task_data=None, error_type="", line_number=0, offset=0):
if task_data is None:
task_data = {}
self.status_code = status_code
self.code = code # a short consistent string describing the error.
self.message = message # A detailed message that provides more information.
self.task_id = task_id or "" # OPTIONAL: The id of the task in the BPMN Diagram.
self.task_name = task_name or "" # OPTIONAL: The name of the task in the BPMN Diagram.
self.file_name = file_name or "" # OPTIONAL: The file that caused the error.
self.tag = tag or "" # OPTIONAL: The XML Tag that caused the issue.
self.task_data = task_data or "" # OPTIONAL: A snapshot of data connected to the task when error occurred.
self.line_number = line_number
self.offset = offset
self.error_type = error_type
if hasattr(g, 'user'):
user = g.user.uid
else:
user = 'Unknown'
self.task_user = user
# This is for sentry logging into Slack
sentry_sdk.set_context("User", {'user': user})
Exception.__init__(self, self.message)
@classmethod
def from_task(cls, code, message, task, status_code=400, line_number=0, offset=0, error_type="", error_line=""):
"""Constructs an API Error with details pulled from the current task."""
instance = cls(code, message, status_code=status_code)
instance.task_id = task.task_spec.name or ""
instance.task_name = task.task_spec.description or ""
instance.file_name = task.workflow.spec.file or ""
instance.line_number = line_number
instance.offset = offset
instance.error_type = error_type
instance.error_line = error_line
# Fixme: spiffworkflow is doing something weird where task ends up referenced in the data in some cases.
if "task" in task.data:
task.data.pop("task")
# Assure that there is nothing in the json data that can't be serialized.
instance.task_data = ApiError.remove_unserializeable_from_dict(task.data)
app.logger.error(message, exc_info=True)
return instance
@staticmethod
def remove_unserializeable_from_dict(my_dict):
keys_to_delete = []
for key, value in my_dict.items():
if not ApiError.is_jsonable(value):
keys_to_delete.append(key)
for key in keys_to_delete:
del my_dict[key]
return my_dict
@staticmethod
def is_jsonable(x):
try:
json.dumps(x)
return True
except (TypeError, OverflowError):
return False
@classmethod
def from_task_spec(cls, code, message, task_spec, status_code=400):
"""Constructs an API Error with details pulled from the current task."""
instance = cls(code, message, status_code=status_code)
instance.task_id = task_spec.name or ""
instance.task_name = task_spec.description or ""
if task_spec._wf_spec:
instance.file_name = task_spec._wf_spec.file
app.logger.error(message, exc_info=True)
return instance
@classmethod
def from_workflow_exception(cls, code, message, exp: WorkflowException):
"""We catch a lot of workflow exception errors,
so consolidating the code, and doing the best things
we can with the data we have."""
if isinstance(exp, WorkflowTaskExecException):
return ApiError.from_task(code, message, exp.task, line_number=exp.line_number,
offset=exp.offset,
error_type=exp.exception.__class__.__name__,
error_line=exp.error_line)
else:
return ApiError.from_task_spec(code, message, exp.sender)
class ApiErrorSchema(ma.Schema):
class Meta:
fields = ("code", "message", "workflow_name", "file_name", "task_name", "task_id",
"task_data", "task_user", "hint", "line_number", "offset", "error_type", "error_line")
@app.errorhandler(ApiError)
def handle_invalid_usage(error):
response = ApiErrorSchema().dump(error)
return response, error.status_code
@app.errorhandler(InternalServerError)
def handle_internal_server_error(e):
original = getattr(e, "original_exception", None)
api_error = ApiError(code='Internal Server Error (500)', message=str(original))
response = ApiErrorSchema().dump(api_error)
return response, 500