Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt ordering exception handlers by exception class inheritance #1514

Closed
vytas7 opened this issue May 6, 2019 · 15 comments
Closed

Attempt ordering exception handlers by exception class inheritance #1514

vytas7 opened this issue May 6, 2019 · 15 comments

Comments

@vytas7
Copy link
Member

vytas7 commented May 6, 2019

At the time of writing, exception handlers are matched in LIFO order, i.e. 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.

In case one has many error handlers for classes potentially inheriting from each other, this may become very error-prone and confusing in case a wrong handler swallows the exception.

Investigate if it is possible to order exceptions statically as they are being added (not at the time of catching).

Prior art: Flask/Werkzeug claim to handle this; investigate how it is done there.

@vytas7 vytas7 added this to the Version 3.0 milestone May 6, 2019
@vytas7 vytas7 added needs contributor Comment on this issue if you'd like to volunteer to work on this. Thanks! needs-decision proposal labels May 6, 2019
@csojinb
Copy link
Contributor

csojinb commented May 7, 2019

Since this is relevant to the other work I did with error-handling, I can dig into werkzeug and try to figure out what's going on there. Don't promise to finish said research though 😅

@csojinb
Copy link
Contributor

csojinb commented May 7, 2019

Here are the relevant lines in Flask for using the most specific handler available. They have some additional levels of categorization for blueprints, or storing handlers by code instead of exception class, which don't seem relevant for Falcon.

So, basically, the error handlers are stored in a dict of {<exception class>: <handler callable>}. Then they iterate through <raised exception>.__mro__ (which produces a tuple of the class hierarchy from least to most general) until a match is found in the error handler dict.

If y'all decide to do this, it seems like it should be pretty straightforward.

@vytas7
Copy link
Member Author

vytas7 commented May 8, 2019

Great investigation, thanks a lot for joining us at sprints, Clara!
We'll discuss what's the best way forwards.

@csojinb there is unfortunately one thing that is complicating the matters a little bit: we have recently added support for handlers associated with more than one exception... 😬 [1]
I was thinking to myself how we could handle this case:

  • Sort handlers by the most (or the least?) general exception in the tuple (should be simple)
  • Split the tuple into separate handlers in case exception classes have different generality scoring
  • (even better alternative that I've missed?)
  1. https://falcon.readthedocs.io/en/stable/api/api.html#falcon.API.add_error_handler

@csojinb
Copy link
Contributor

csojinb commented May 8, 2019

@vytas7 I might be misunderstanding the issue here... what is the problem with having handlers associated with more than one exception? Can't you just store that as {Exception1: handle_exceptions_1_and_2, Exception2: handle_exceptions_1_and_2}?

@vytas7
Copy link
Member Author

vytas7 commented May 13, 2019

@csojinb No, there is no problem per se, that is what my suggestion 2 is basically aiming at.
You could indeed store expanded handlers in the way you depict, however, if possible, for performance reasons it would be good to keep exceptions grouped together in a tuple in case they sort equal on the generality scale, and have a shared handler.

@kgriffs kgriffs modified the milestones: Version 3.0, Version 3.1 Aug 28, 2019
@kgriffs
Copy link
Member

kgriffs commented Oct 29, 2019

Where did we end up on this issue? Is #1527 still waiting on this?

@vytas7
Copy link
Member Author

vytas7 commented Oct 29, 2019

@kgriffs this is, strictly speaking, not blocking #1527 .
But it would make the implementation more straightforward and consistent by not having to care the default bare handler is invoked last (unless I completely misunderstood @csojinb 's comment)

@csojinb
Copy link
Contributor

csojinb commented Oct 29, 2019

The relevance of this to #1527 is that the current LIFO order may result in surprising behavior if someone overrides the Exception handler, which is documented here.

Personally, I don't think this issue needs to block #1527 . IIUC, Falcon users who add a custom handler for Exception will already be overriding the default HTTPError and HTTPStatus handlers, so I don't think it's a dealbreaker to not solve that problem in order to address #1507.

@kgriffs kgriffs modified the milestones: Version 3.1, Version 3.0 Nov 1, 2019
@kgriffs
Copy link
Member

kgriffs commented Nov 1, 2019

OK, I'd love to have this smarter ordering implemented for 3.0 if we can swing it, otherwise push to 3.1.

@kgriffs
Copy link
Member

kgriffs commented Nov 1, 2019

Any volunteers? :)

@kgriffs
Copy link
Member

kgriffs commented Nov 1, 2019

P.S. When this is implemented, don't forget to update the note under the add_error_handler() docstring.

@vytas7
Copy link
Member Author

vytas7 commented Nov 1, 2019

I think Falcon users who (read: me 🙂 but I think @jmvrbanac also uses this pattern and has even advised users to do so) add a custom exception handler for Exception may also be re-raising HTTPError & HTTPStatus which are then caught in a special clause in API._handle_exception.

Not sure if we need to do much about this now though. The problematic should be gone once we introduce exception ordering.

csojinb added a commit to csojinb/falcon that referenced this issue Nov 2, 2019
Addresses falconry#1514

Rather than selecting error handlers in LIFO order of registration,
select the error handler corresponding to the nearest direct
ancestor of the exception type raised. So, for example, if an app
only adds a custom error handler for the Python ``Exception`` class,
and an ``HTTPForbidden`` error is raised, then we use the
default handler for ``HTTPError`` rather than the more general
``Exception`` handler (which is the pre-existing behavior).

This is implemented by storing error handlers on the API object
as a dict rather than a list and looking them up using the method
resolution order attribute (`__mro__`) on the raised exception
class.

NOTE: This commit only includes the actual implementation and does
not address testing or documentation. I am seeking implementation
feedback before completing those additional changes.

BREAKING CHANGE: Registration of a new error handler for type E
will no longer override previously-registered error handlers for
subclasses of type E. Registration order will no longer matter
*except* when multiple error handlers are registered for the
exact same exception type, in which case the most recently
registered error handler overrides the previous ones.
@vytas7 vytas7 removed needs contributor Comment on this issue if you'd like to volunteer to work on this. Thanks! needs-decision proposal labels Nov 2, 2019
@csojinb
Copy link
Contributor

csojinb commented Nov 2, 2019

Hi, I put up a draft PR with an implementation for this. I was hoping to get some input on the approach before adding tests, etc.

I went with the simpler implementation because I didn't totally understand @vytas7's first suggestion for managing shared error handlers. Should I do a benchmark comparison or something for this implementation?

@vytas7
Copy link
Member Author

vytas7 commented Nov 2, 2019

Scratch those suggestions. Your proposed approach is way more scalable in the general case I think 👍 (it is only adding complexity traversing the MRO, but the class lookup itself is dict-based (O(1)-ish)).

Benchmark against the current implementation would nevertheless be good to take a look at. (As said, I think those are going to be in favour of your new approach)

csojinb added a commit to csojinb/falcon that referenced this issue Nov 2, 2019
Addresses falconry#1514

Rather than selecting error handlers in LIFO order of registration,
select the error handler corresponding to the nearest direct
ancestor of the exception type raised. So, for example, if an app
only adds a custom error handler for the Python ``Exception`` class,
and an ``HTTPForbidden`` error is raised, then we use the
default handler for ``HTTPError`` rather than the more general
``Exception`` handler (which is the pre-existing behavior).

This is implemented by storing error handlers on the API object
as a dict rather than a list and looking them up using the method
resolution order attribute (`__mro__`) on the raised exception
class.

NOTE: This commit only includes the actual implementation and does
not address testing or documentation. I am seeking implementation
feedback before completing those additional changes.

BREAKING CHANGE: Registration of a new error handler for type E
will no longer override previously-registered error handlers for
subclasses of type E. Registration order will no longer matter
*except* when multiple error handlers are registered for the
exact same exception type, in which case the most recently
registered error handler overrides the previous ones.
csojinb added a commit to csojinb/falcon that referenced this issue Dec 15, 2019
Addresses falconry#1514

Rather than selecting error handlers in LIFO order of registration,
select the error handler corresponding to the nearest direct
ancestor of the exception type raised. So, for example, if an app
only adds a custom error handler for the Python ``Exception`` class,
and an ``HTTPForbidden`` error is raised, then we use the
default handler for ``HTTPError`` rather than the more general
``Exception`` handler (which is the pre-existing behavior).

This is implemented by storing error handlers on the API object
as a dict rather than a list and looking them up using the method
resolution order attribute (`__mro__`) on the raised exception
class.

NOTE: This commit only includes the actual implementation and does
not address testing or documentation. I am seeking implementation
feedback before completing those additional changes.

BREAKING CHANGE: Registration of a new error handler for type E
will no longer override previously-registered error handlers for
subclasses of type E. Registration order will no longer matter
*except* when multiple error handlers are registered for the
exact same exception type, in which case the most recently
registered error handler overrides the previous ones.
vytas7 pushed a commit that referenced this issue Dec 16, 2019
…#1603)

* doc(_handle_error): update NOTE missed in #1527

`_handle_error` returns false when the exception does not match a
custom handler and is not a subclass of ``HTTPError``, ``HTTPStatus``,
or ``Exception``. The update to include ``Exception`` was missed in
PR #1527, which added a default handler for ``Exception``

* feat(API): on exception, select most specific error handler available

Addresses #1514

Rather than selecting error handlers in LIFO order of registration,
select the error handler corresponding to the nearest direct
ancestor of the exception type raised. So, for example, if an app
only adds a custom error handler for the Python ``Exception`` class,
and an ``HTTPForbidden`` error is raised, then we use the
default handler for ``HTTPError`` rather than the more general
``Exception`` handler (which is the pre-existing behavior).

This is implemented by storing error handlers on the API object
as a dict rather than a list and looking them up using the method
resolution order attribute (`__mro__`) on the raised exception
class.

NOTE: This commit only includes the actual implementation and does
not address testing or documentation. I am seeking implementation
feedback before completing those additional changes.

BREAKING CHANGE: Registration of a new error handler for type E
will no longer override previously-registered error handlers for
subclasses of type E. Registration order will no longer matter
*except* when multiple error handlers are registered for the
exact same exception type, in which case the most recently
registered error handler overrides the previous ones.

* test: add test for new error-handling order

* doc(App): Document new error handler precedence logic

* doc(add_error_handler): add versionchanged directive

For the change in error-handler-match strategy

* doc(towncrier): add feature news fragment for error handler precedence
@vytas7
Copy link
Member Author

vytas7 commented Dec 16, 2019

Resolved in #1603
Thanks @csojinb ! 🎉

@vytas7 vytas7 closed this as completed Dec 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants