Exception handling

Created:
April 24, 2023
Updated:
October 31, 2023

Rendering Exceptions through the Flask Handler

Flask, by default contains an exception handler, which FireTail’s app can proxy to with the add_error_handler method. You can hook either on status codes or on a specific exception type.

FireTail is moving from returning flask responses on errors to throwing exceptions that are a subclass of firetail.problem. So far exceptions thrown in the OAuth decorator have been converted.

Flask Error Handler Example

The goal here is to make the API returning the 404 status code when there is a NotFoundException (instead of 500).


def test_should_return_404(client):
    invalid_id = 0
    response = client.get(f"/api/data/{invalid_id}")
    assert response.status_code == 404

Firstly, it’s possible to declare what Exception must be handled:


# exceptions.py
class NotFoundException(RuntimeError):
    """Not found."""

class MyDataNotFound(NotFoundException):
    def __init__(self, id):
        super().__init__(f"ID '{id}' not found.")


# init flask app
import firetail

def not_found_handler(error):
    return {
        "detail": str(error),
        "status": 404,
        "title": "Not Found",
    }, 404

def create_app():

    firetail_app = firetail.FlaskApp(
        __name__, specification_dir="../api/")
    firetail_app.add_api(
        "openapi.yaml", validate_responses=True,
        base_path="/")

    # Handle NotFoundException
    firetail_app.add_error_handler(
        NotFoundException, not_found_handler)

    app = firetail_app.app
    return app

In this way, it’s possible to raise anywhere the NotFoundException or its subclasses and we know the API will return 404 status code.


from sqlalchemy.orm.exc import NoResultFound

from .exceptions import MyDataNotFound
from .models import MyData


def get_my_data(id, token_info=None):
    try:
        data = MyData.query.filter(MyData.id == id).one()

        return {
            "id": data.id,
            "description": data.description,
        }

    except NoResultFound:
        raise MyDataNotFound(id)

Default Exception Handling

By default, FireTail exceptions are JSON serialized according to Problem Details for HTTP APIs

The application can return errors using firetail.problem or exceptions that inherit from both firetail.ProblemException and a werkzeug.exceptions.HttpException subclass (for example werkzeug.exceptions.Forbidden). An example of this is the firetail.exceptions.OAuthProblem exception:


class OAuthProblem(ProblemException, Unauthorized):
    def __init__(self, title=None, **kwargs):
        super(OAuthProblem, self).__init__(title=title, **kwargs)

Examples of Custom Rendering Exceptions

To custom render an exception when you boot your FireTail application you can hook into a custom exception and render it in some sort of custom format. For example:


rom flask import Response
import firetail
from firetail.exceptions import OAuthResponseProblem

def render_unauthorized(exception):
    return Response(response=json.dumps({'error': 'There is an error in the oAuth token supplied'}), status=401, mimetype="application/json")

app = firetail.FlaskApp(__name__, specification_dir='./../swagger/', debug=False, swagger_ui=False)
app.add_error_handler(OAuthResponseProblem, render_unauthorized)

Custom Exceptions

There are several exception types in FireTail that contain extra information to help you render appropriate messages to your user beyond the default description and status code:

  • OAuthProblem - This exception is thrown when there is some sort of validation issue with the Authorisation Header.
  • OAuthResponseProblem - this exception is thrown when there is a validation issue from your OAuth 2 Server. It contains a token_response property which contains the full HTTP response from the OAuth 2 Server.
  • OAuthScopeProblem - This scope indicates the OAuth 2 Server did not generate a token with all the scopes required. This contains 3 properties - required_scopes - The scopes that were required for this endpoint - token_scopes - The scopes that were granted for this endpoint.

Related topics