Added section for Exceptions and Error Handling
This commit is contained in:
parent
e8c213ef6d
commit
b4fd440829
|
@ -29,4 +29,4 @@ and use it to learn about the different parts of the CR Connect Workflow code ba
|
|||
01_scripts
|
||||
02_services
|
||||
03_api
|
||||
|
||||
04_errors
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
=============================
|
||||
Exceptions and Error Handling
|
||||
=============================
|
||||
|
||||
CR Connect Workflow has a custom Exception defined in crc.api.common, and we use it throughout the backend.
|
||||
It allows us to manage error reporting in one place, and format errors for the front-end.
|
||||
|
||||
--------
|
||||
ApiError
|
||||
--------
|
||||
|
||||
**ApiError** is defined in crc.api.common.
|
||||
|
||||
It has 2 required arguments; `code` and `message`.
|
||||
|
||||
By convention, **code** is short, all lower case, and separated by underscores.
|
||||
It is meant to be read by code.
|
||||
|
||||
**Message** is meant to be read by a person, such as a user, configurator, or programmer.
|
||||
It can be longer form, and should include information about the context of the error when possible.
|
||||
|
||||
The other arguments allow us to pass along additional information depending on the context.
|
||||
|
||||
.. code-block::
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, code, message, status_code=400,
|
||||
file_name="", task_id="", task_name="", tag="", 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 ocurred.
|
||||
if hasattr(g,'user'):
|
||||
user = g.user.uid
|
||||
else:
|
||||
user = 'Unknown'
|
||||
self.task_user = user
|
||||
|
||||
|
||||
------------------------
|
||||
Helper Methods
|
||||
------------------------
|
||||
|
||||
ApiError has 2 useful methods; **from_task** and **from_task_spec**.
|
||||
|
||||
If an error occurs in the context of a task or task_spec,
|
||||
these methods allows us to include more information about the error.
|
||||
|
||||
TODO: task vs task spec
|
||||
|
||||
Notice that these are class methods.
|
||||
|
||||
from_task
|
||||
---------
|
||||
|
||||
In addition to `code` and `message`, from_task requires a **task** argument.
|
||||
|
||||
.. code-block::
|
||||
|
||||
@classmethod
|
||||
def from_task(cls, code, message, task, 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.task_spec.name or ""
|
||||
instance.task_name = task.task_spec.description or ""
|
||||
instance.file_name = task.workflow.spec.file or ""
|
||||
|
||||
instance.task_data = task.data
|
||||
app.logger.error(message, exc_info=True)
|
||||
return instance
|
||||
|
||||
|
||||
from_task_spec
|
||||
--------------
|
||||
|
||||
In addition to `code` and `message`, from_task_spec requires a **task_spec** argument.
|
||||
|
||||
.. code-block::
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
-------------
|
||||
Example Usage
|
||||
-------------
|
||||
|
||||
Using from_task
|
||||
---------------
|
||||
|
||||
This example comes from WorkflowService._process_documentation in crc/services/workflow_service.py.
|
||||
|
||||
We trap two specific errors--TemplateError and TypeError, and raise an ApiError using from_task.
|
||||
We also catch a general Exception error and log it.
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
||||
try:
|
||||
template = Template(raw_doc)
|
||||
return template.render(**spiff_task.data)
|
||||
|
||||
except jinja2.exceptions.TemplateError as ue:
|
||||
raise ApiError.from_task(code="template_error",
|
||||
message="Error processing template for task %s: %s" %
|
||||
(spiff_task.task_spec.name, str(ue)), task=spiff_task)
|
||||
|
||||
except TypeError as te:
|
||||
raise ApiError.from_task(code="template_error",
|
||||
message="Error processing template for task %s: %s" %
|
||||
(spiff_task.task_spec.name, str(te)), task=spiff_task)
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(str(e), exc_info=True)
|
||||
|
||||
|
||||
Call ApiError directly
|
||||
----------------------
|
||||
|
||||
In this example from crc/services/workflow_processor.py,
|
||||
we call ApiError directly, without using from_task or from_task_spec.
|
||||
|
||||
.. code-block::
|
||||
|
||||
except SyntaxError as e:
|
||||
raise ApiError('syntax_error',
|
||||
f'Something is wrong with your python script '
|
||||
f'please correct the following:'
|
||||
f' {script}, {e.msg}')
|
||||
except NameError as e:
|
||||
raise ApiError('name_error',
|
||||
f'something you are referencing does not exist:'
|
||||
f' {script}, {e}')
|
||||
|
||||
|
||||
--------------
|
||||
ApiErrorSchema
|
||||
--------------
|
||||
|
||||
CR Connect Workflow defines another class in crc.api.common that helps us format ApiError output as JSON so we can return it to the frontend
|
||||
|
||||
.. code-block::
|
||||
|
||||
class ApiErrorSchema(ma.Schema):
|
||||
class Meta:
|
||||
fields = ("code", "message", "workflow_name", "file_name", "task_name", "task_id",
|
||||
"task_data", "task_user", "hint")
|
||||
|
||||
In the class, we list the fields we want to include in the returned JSON.
|
||||
|
||||
We then use a hook provided by the api to format the output. This code is in crc/__init__.py
|
||||
|
||||
.. code-block::
|
||||
|
||||
# Connexion Error handling
|
||||
def render_errors(exception):
|
||||
from crc.api.common import ApiError, ApiErrorSchema
|
||||
error = ApiError(code=exception.title, message=exception.detail, status_code=exception.status)
|
||||
return Response(ApiErrorSchema().dump(error), status=401, mimetype="application/json")
|
||||
|
||||
connexion_app.add_error_handler(ProblemException, render_errors)
|
||||
|
||||
|
||||
--------------------
|
||||
Unhandled Exceptions
|
||||
--------------------
|
||||
|
||||
CR Connect Workflow uses a feature of flask to capture unhandled exceptions.
|
||||
In crc.api.common, we define a handler for InternalServerError and add a call to ApiError.
|
||||
|
||||
.. code-block::
|
||||
|
||||
@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
|
||||
|
||||
|
||||
----
|
||||
More
|
||||
----
|
||||
|
||||
More about Python error handling: https://docs.python.org/3/tutorial/errors.html#handling-exceptions
|
||||
|
||||
More about Flask error handling: https://flask.palletsprojects.com/en/1.1.x/errorhandling/
|
Loading…
Reference in New Issue