Falcon: No easy way to add generic Exception handler using add_error_handler()

Created on 14 Nov 2016  路  7Comments  路  Source: falconry/falcon

If I want to add a generic error handler to handle Exception, we cannot use add_error_handler() because it will override the falcon.HTTPError and falcon.HTTPStatus handlers. The only way I can think out is:

application = falcon.API()
application._error_handlers.append((Exception, handler))

One suggestion is to expose _error_handlers as a property error_handlers and remove function add_error_handler().

duplicate enhancement

Most helpful comment

Generic handlers can be problematic. You've requested the handler to handle Exception and its sub-classes. Since falcon.HTTPError inherits from Exception, it's technically doing what it's suppose to. However, you can still do what you're trying to do. You just have to tell the handler to not mess with HTTPError. So something like:

def generic_error_handler(ex, req, resp, params):
    # Ignore HTTPError and re-raise
    if isinstance(ex, HTTPError):
        raise

    # Do things with other errors here

app = falcon.API()
app.add_error_handler(Exception, generic_error_handler)

Hope this helps!

All 7 comments

Generic handlers can be problematic. You've requested the handler to handle Exception and its sub-classes. Since falcon.HTTPError inherits from Exception, it's technically doing what it's suppose to. However, you can still do what you're trying to do. You just have to tell the handler to not mess with HTTPError. So something like:

def generic_error_handler(ex, req, resp, params):
    # Ignore HTTPError and re-raise
    if isinstance(ex, HTTPError):
        raise

    # Do things with other errors here

app = falcon.API()
app.add_error_handler(Exception, generic_error_handler)

Hope this helps!

Thanks for the tip.

Not every exception will be handled by HTTPError:

>>> from falcon import HTTPError
>>> ex = Exception('')
>>> isinstance(ex, HTTPError)
False
>>> ex = OSError('')
>>> isinstance(ex, HTTPError)
False

I think people do need to setup a catch-all error handler. My workaround and your suggestion all seems a hack. As far as we have internal default values for _error_handlers, one add_error_handler() seems not flexible enough.

From interface's point of view, if we remove default values, only one add_error_handler() should be enough, but I don't think we should do it as it will break applications. So the suggestion is obsolete add_error_handler() and expose error_handlers as a property.

Error handlers are matched in LIFO order. In other words, when searching for an error handler to match a raised exception, and more than one handler matches the exception type, the framework will choose the one that was most recently registered. Therefore, more general error handlers (e.g., for the standard Exception type) should be added first, to avoid masking more specific handlers for subclassed types.

Really the problem is that the documentation gives advice that you can't reliably follow and expect to work as it should. The reason is that API.__init__() calls add_error_handler, and so if you do as suggested in the docs, your custom exception handler for Exception gets called for everything because HTTPError and HTTPStatus were registered first and so are "last out".

Our current use case is that we want to report unexpected exceptions (i.e. something that isn't HTTPError or a subclass) to our monitoring service. The easy thing to do is to add an error handler that will catch everything that's not already an HTTPError or HTTPStatus and report the exception instead of trying to wrap everything in try/except.

Seems like the only viable solution is to let users manipulate the list directly, since since one might want to insert handlers on either side of the defaults, and there appears to be no way to override how they get added to the list.

Thanks @foresmac for looking at this. I agree exposing the list directly will give user the most flexibility. The original author may want to expose a smaller interface which myself may do too.

I do get a workaround very like @jmvrbanac suggested for what I need:

app.add_error_handler(Exception, errors.internal_error_handler)

def internal_error_handler(e, req, resp, params):
    if not isinstance(e, (falcon.HTTPError, falcon.HTTPStatus)):
        e = falcon.HTTPInternalServerError(description=str(e))
    logging.exception(e)
    raise e

This handler will catch all exceptions; the default handlers are ignored. It will also convert the exception into HTTPError if it's not in (falcon.HTTPError, falcon.HTTPStatus). Then the new exception will be handled by self._compose_error_response or self._compose_status_response(req, resp, status)

It's a little trick, but works for my need.

I'm OK to keep the current design until someone requests another unsupported use case.

I did something similar, which so far seems to work as expected, but it's not especially intuitive unless you understand that the handlers for HTTPError and HTTPStatus get called if an error handler raises one of those. The intent here is to forward all exceptions raised to Stackdriver:

def stackdriver_reporting_handler(ex, req, resp, params):
    status_code = int(ex.status[:3]) if isinstance(ex, (HTTPError, HTTPStatus)) else 500
    user = req.context.get('user')
    http_context = build_falcon_context(req, status_code)

    report_exception(http_context, user)
    raise

Used the naked raise so that the original stacktrace is maintained (at least on Python 3.5+, IIRC). Any uncaught exception still turns into a 500 by the front-end web server (e.g. gunicorn, etc).

this has worked for me...altough I'm not sure exactly how.

class ErrorHandler:
    @staticmethod
    def http(ex, req, resp, params):
        raise

    @staticmethod
    def unexpected(ex, req, resp, params):
        logger.exception('Unexpected Application Error')
        raise falcon.HTTPInternalServerError()

def get_app():
    app = falcon.API()
    app.add_error_handler(Exception, ErrorHandler.unexpected)
    app.add_error_handler(falcon.HTTPError, ErrorHandler.http)
    app.add_error_handler(falcon.HTTPStatus, ErrorHandler.http)

We hope to get https://github.com/falconry/falcon/issues/1514 done within the 3.0 release timeframe. There is even an open PR (https://github.com/falconry/falcon/pull/1603) out there implementing the feature.

I'm thus going to close this issue in favour of https://github.com/falconry/falcon/issues/1514 hoping it would offer a reasonably robust solution for the case discussed here too, but do not hesitate to reopen if needed, or provide feedback on https://github.com/falconry/falcon/issues/1514 / https://github.com/falconry/falcon/pull/1603!

Was this page helpful?
0 / 5 - 0 ratings