Search code examples
pythonflaskexceptionflask-restful

Flask Error handling not working properly


I have a custom exception shown below where functions are raising these exceptions.

class UnauthorizedToSendException(HTTPException):
    code = 400
    description = 'Unauthorized to Send.'

They are then defined in Flask's create_app() as follows:

def handle_custom_exceptions(e):
    response = {"error": e.description, "message": ""}
    if len(e.args) > 0:
        response["message"] = e.args[0]
    # Add some logging so that we can monitor different types of errors
    app.logger.error(f"{e.description}: {response['message']}")
    return jsonify(response), e.code

 app.register_error_handler(UnauthorizedToSendException, handle_custom_exceptions)

When this exception is raised below:

class LaptopStatus(Resource):
    @staticmethod
    def get():
        raise UnauthorizedToSendException('you are unauthorized to send')

However, the output is always this way:

{
    "message": "you are unauthorized to send"
}

Is there something missing here?


Solution

  • flask_restful.Api has its own error handling implementation that mostly replaces Flask's errorhandler functionality, so using the Flask.errorhandler decorator or Flask.register_error_handler won't work.

    There are a couple solutions to this.

    Don't Inherit from HTTPException and set PROPAGATE_EXCEPTIONS

    The flask-restful error routing has short circuits for handling exceptions that don't inherit from HTTPException, and if you set your Flask app to propagate exceptions, it will send the exception to be handled by normal Flask's handlers.

    import flask
    from flask import Flask
    from flask_restful import Resource, Api
    
    
    app = Flask(__name__)
    
    # Note that this doesn't inherit from HTTPException
    class UnauthorizedToSendException(Exception):
        code = 400
        description = 'Unauthorized to Send'
    
    
    @app.errorhandler(UnauthorizedToSendException)
    def handle_unauth(e: Exception):
        rsp = {"error": e.description, "message": ""}
        if len(e.args) > 0:
            rsp["message"] = e.args[0]
    
        app.logger.error(f"{e.description}: {rsp['message']}")
        return flask.jsonify(rsp), e.code
    
    
    class LaptopStatus(Resource):
        @staticmethod
        def get():
            raise UnauthorizedToSendException("Not authorized")
    
    
    api = Api(app)
    api.add_resource(LaptopStatus, '/status')
    
    
    if __name__ == "__main__":
        # Setting this is important otherwise your raised
        # exception will just generate a regular exception
        app.config['PROPAGATE_EXCEPTIONS'] = True
        app.run()
    

    Running this I get...

    #> python flask-test-propagate.py
    # ... blah blah ...
    
    
    #> curl http://localhost:5000/status
    {"error":"Unauthorized to Send","message":"Not authorized"}
    

    Replace flask_restul.Api.error_router

    This will override the behavior of the Api class's error_router method, and just use the original handler that Flask would use.

    You can see it's just a monkey-patch of the class's method with...

    Api.error_router = lambda self, hnd, e: hnd(e)
    

    This will allow you to subclass HTTPException and override flask-restful's behavior.

    import flask
    from flask import Flask
    from flask_restful import Resource, Api
    from werkzeug.exceptions import HTTPException
    
    # patch the Api class with a custom error router
    # that just use's flask's handler (which is passed in as hnd)
    Api.error_router = lambda self, hnd, e: hnd(e)
    
    app = Flask(__name__)
    
    
    class UnauthorizedToSendException(HTTPException):
        code = 400
        description = 'Unauthorized to Send'
    
    
    @app.errorhandler(UnauthorizedToSendException)
    def handle_unauth(e: Exception):
        print("custom!")
        rsp = {"error": e.description, "message": ""}
        if len(e.args) > 0:
            rsp["message"] = e.args[0]
    
        app.logger.error(f"{e.description}: {rsp['message']}")
        return flask.jsonify(rsp), e.code
    
    
    class LaptopStatus(Resource):
        @staticmethod
        def get():
            raise UnauthorizedToSendException("Not authorized")
    
    
    api = Api(app)
    api.add_resource(LaptopStatus, '/status')
    
    
    if __name__ == "__main__":
        app.run()
    

    Switch to using flask.views.MethodView

    Documentation:

    Just thought of this, and it's a more drastic change to your code, but flask has facilities for making building REST APIs easier. flask-restful isn't exactly abandoned, but the pace of changes have slowed down the last several years, and various components like the error handling system have become too inflexible.

    If you're using flask-restful specifically for the Resource implementation, you can switch to the MethodView, and then you don't need to do any kind of workarounds.

    import flask
    from flask import Flask
    from flask.views import MethodView
    from werkzeug.exceptions import HTTPException
    
    
    app = Flask(__name__)
    
    
    class UnauthorizedToSendException(HTTPException):
        code = 400
        description = 'Unauthorized to Send'
    
    
    @app.errorhandler(UnauthorizedToSendException)
    def handle_unauth(e: Exception):
        rsp = {"error": e.description, "message": ""}
        if len(e.args) > 0:
            rsp["message"] = e.args[0]
    
        app.logger.error(f"{e.description}: {rsp['message']}")
        return flask.jsonify(rsp), e.code
    
    
    class LaptopStatusApi(MethodView):
        def get(self):
            raise UnauthorizedToSendException("Not authorized")
    
    
    app.add_url_rule("/status", view_func=LaptopStatusApi.as_view("laptopstatus"))
    
    if __name__ == "__main__":
        app.run()