-
-
Notifications
You must be signed in to change notification settings - Fork 16.3k
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
errorhandler should expect exception classes instead of exception instances #1281
Conversation
First off, thanks for your contribution!
|
Honestly I don't care that much about fixing this in 0.10, so ignore the second bulletpoint. |
i wrote this ad-hoc without testing locally, just to get it done before leaving that day. i’ll fix it probably this evening… |
ahah! indeed the user exception path is taken in the tests. user exceptions work by registering a list of this is important, because the wrong check i tried to fix leads to the registering of a user exception, and this way, the existing tests pass. which they shouldn’t, because: what the defunct code path that currently is never triggered would do is that it’d simply extract the error code from the exception, which would lead to a replacement of that error handler (so if issubclass(code_or_exception, HTTPException): # current code tests isinstance instead.
code = code_or_exception.code
elif isinstance(code_or_exception, integer_types):
code = code_or_exception
self.error_handler_spec[key][code] = f # there can be only one! some examplessetup:
the three paths (what they do now):
the third path (after my naive patch) would then also replace the 418 error handler, which would break the tests, because again… summarythe tests are broken because they expect HTTPException subclasses to behave like user exceptions, i.e. registering a HTTPException subclass would not replace the HTTPExceptionSubclass.code handler. the errorhandler registration code is broken so it actually does make HTTPException subclasses behave like user exceptions. @mitsuhiko i’m unsure how to proceed. was it really intended than any kind of HTTPException subclass handler replaces the blueprint/app wide one? or what? |
approaches: fuck itremove special tests and code path for HTTPException subclasses. treat all exceptions the same, except that e.g. a instance of ForbiddenSubclass will trigger the 403 handler if no ForbiddenSubclass handler is registered. cons: apparently this is not behavior intended by the code. also it might get ugly with subclasses of subclasses, because isinstance checks are performed in sequence, not specificallity “fix” itfix the tests to actually reflect what the code tries to do, i.e. replacement of the 403 error handler if a ForbiddenSubclass or ForbiddenSubclass2 handler is registered. cons: surprising! really fix itcreate a subclass registry for every code that is searched (if exception is an instance of HTTPException, search the exception.code handler registry, not the user exception registry) also filter the registered exception classes by which are superclasses of the thrown one, and call the handler for the most specific one! replace cons: most complex ??maybe someone who had the idea about what should happen can weigh in… |
Fuck-it seems like the most attractive of all of them. I'd be very interested in a complete re-write of exception handling, given @mitsuhiko's consent. |
well, wouldn’t that rewrite basically be really-fix-it? or do you have different plans? i’d envision that the handler flow would be (pseudocode): if is_http_exception(exception):
code = 'user'
else:
code = exception.code
handler = app.error_handler_spec[blueprint][code].find_most_specific_for(exception)
if not handler:
handler = app.error_handler_spec[None][code].find_most_specific_for(exception) with the invariant that fallthroughs would be (naturally) the least specific handler for that error code, i.e. the “http_exceptions.Forbidden” handler for any |
IMO a rewrite would simply work the same way Python's exception-handling works: There are no "403-handlers" (or any handlers for status codes), just handlers for exceptions. If there is no handler for ForbiddenSubclass, the handler for Forbidden would be used. |
well first off, yeah, codes and classes would be synonymous: but moving on, i’m not exactly sure what you mean: in python, an exception handler is a try/except clause. the except clause that first matches the exception while it’s bubbling up the stack wins. but we want to register exception handlers via decorators, so the intuition is: error handlers are independently from stack depth and order. at least my intuition is that module-level decorators are order-independent: i don’t expect the order in which i place my functions to have any impact, except when one is an override of the other. so i guess the least surprising thing to do is: class ForbiddenSub(Forbidden): pass
@app.errorhandler(ForbiddenSubClass)
def x(e): ...
@app.errorhandler(403)
def y(e): ...
#replaces y because synonymous. we should throw a warning here
@app.errorhandler(Forbidden)
def z(e): ...
@app.errorhandler(500)
def w(e): ...
def test_a():
raise Forbidden()
# triggers z, since z replaces y
def test_b():
raise ForbiddenSubclass()
# triggers x, because x is more specific than z (the Forbidden handler)
def test_c():
raise TypeError()
# user exception triggers the debugger or “w”, depending on debug settings
def test_d():
raise ImATeapot()
# triggers the default 418 handler since we defined none and blueprints work the same, except that there are no blueprint-wide defaults: any class of Exceptions (e.g. Forbidden subclasses or user exceptions) will be routed to the app’s handlers if there are no fitting handlers registered on the blueprint. |
With "like Python" I mostly meant to match only by exception type and not any codes. |
That's what I am suggesting. I think the algorithm for |
I disagree. That's completely counter intuitive: @app.errorhandler(BarException)
def handle_bar(e):
...
@app.errorhandler(FooException)
def handle_foo(e):
...
@app.route('/')
def index():
raise FooException() What Do you mean the BarException handler is called? |
Yes, but it's how python exception handling works, and how Flask's currently does too. |
no. afaik there is no concept of error handlers, and there the sequence in which except clauses are testing is bound to the stack: the first except clause up from the exception raising wins. we have declarative code here, with decorators, no call stack. no verticality, no bubbling. nothing of those things is exposed to the decorators. do you get where i’m coming from? :)
maybe, but nobody seems to use it in an advanced way if i’m the first one realizing that finally, for subclassing exception the first registered handler wins, as in above example: if X is a subclass of Y, and you register a Y, then X handler, the X handler will never be used even if you throw a X instance. and for codes, the last registered handler wins: if you register a handler for 403, and then one for forbidden_subclass_instance, the latter will get all 403s. what i’m getting at: the whole errorhandler system is unintuitive, complex, and in places broken. my simple idea is: the most specific error handler wins. the intuitive specificity sequence is
i bet there is a very efficient data structure for finding the most specific superclass of an object out of a set of registered ones. |
I am only talking about the "algorithm" used for prioritization:
As opposed to finding the most specific error handler (or except-clause) for an exception.
That's a really strong assumption, and I think it's impossible to avoid advanced usage in any sufficiently complex Flask app. Note that I am not really opposed to a new behavior, but I can't really imagine that the benefit of "intuitiveness" would outweigh the new learning curve. Because, while ordering by registration time might seem counterintuitive, the behavior is easily explained, while a more complex ranking algorithm probably requires much more learning effort. For example, even with "specifity" defined, how should ties be resolved? Sorting by registration time passes that responsibility off to the user.
It does work for me (yes, with all the bugs), it doesn't with the bugs fixed. |
OK, gotcha. i still think that the mental model of registering handlers via decorators is horizontal and declarative, while the mental model with handlers is tied to the structure of the code, i.e. the call stack and is therefore vertical and sequential. also in our case, handlers are endpoints, as in they return responses, whereas except clauses are just there in the code and either let things flow on or reraise the exceptions. both of those things reinforce the disparate mental models associated with the approaches. apart from reacting to exceptions, those two approaches have nothing in common, look and feel very different. and again: top level function definitions (unlke nested ones inside of a function) have a order-independent feel to them, as have decorators. and every API i ever used made that work, by only breaking that mental model if you override something existing (mostly accidentally) i think my idea is just adequate API design, whereas the way things work right now is surprising for everything but simple cases (where it works identically to my approach)
well, it still comes down to registration order when there are overrides and times. i just think that this behavior (in a ”note” box in the docs) is still more intuitive than e.g. following scenario: app = Flask(__name__)
[...]
app.errorhandler(403)
def default_forbidden():
return 'You’re not allowed to do that', 403 and somewhere else @app.errorhandler(CheatException)
def cheat(e):
return '''
You thought you’d get through with faking your score
while it really is {e.real_score}
'''.format(e=e), CheatException.code
@app.route('/submit_score/<score>')
def submit_score(score):
if not internal_score == score:
raise CheatException(internal_score) Now add templates and more complexity and have fun debugging why you raised a CheatException for which you clearly just defined a handler (it’s right there, for god’s sake!) and still some nondescript 403 page came!
huh? my pull request is incomplete and naive, made at a point where i thought the rest of the code worked as it seemed like it should, but it turns out that the tests rely on broken functionality, thus our discussion here. what do you actually use? what works? what doesn’t? what fixed bugs are you talking about? |
by the way: i found a very simple and efficient implementation for my idea (example only with app, not blueprint). half-pseudocode prototype: class DefaultHandlers(object):
def __getitem__(self, code):
description = http_codes.get(code)
if description is not None:
return lambda e: '<h1>Error {}</h1>\n{}<br>{}'.format(code, description, e)
else:
raise KeyError(code)
Flask.handlers = ChainMap({}, DefaultHandlers())
# for blueprint: ChainMap({}, app.handlers, DefaultHandlers())
def is_http_exception(error_class_or_instance):
return (
isinstance(error_class_or_instance, HTTPException) or
isinstance(error_class_or_instance, type) and
issubclass(error_class_or_instance, HTTPException))
def Flask.get_handlers(self, e):
code = e.code if is_http_exception(e) else None # None: user exception
return self.handlers.setdefault(code, {})
def Flask.register_errorhandler(self, error_class, handler_function):
assert issubclass(error_class, Exception)
self.get_handlers(error_class)[error_class] = handler_function
def Flask.find_appropriate_handler(e):
assert isinstance(error_class, Exception)
handlers = self.get_handlers(e)
for error_class in type(e).mro():
handler = handlers.get(error_class)
if hander is not None:
return handler(e) |
If I register an errorhandler for ForbiddenSubclass before registering less specific error handlers, it works as one would expect:
In summary: Feel free to open a PR with a draft of your proposed new behavior. Maybe it works, maybe it doesn't... your suggestions seem sound, but I am honestly unsure of the consequences regarding backwards compatibility. At this point I think only coding it up would make this clear. |
well, what you say will still work after that of course. what won’t work is
|
Go ahead, I'd have to play around with the implementation to be able to make any statements. |
FWIW, I've never once thought to register custom |
but if you like in my example: if you raise CheatException (subclass of Forbidden), you only get the handler registered for 403. |
If you put them in the correct order, your goal is possible:
|
That is, with the master branch. |
No, you'll see whatever your custom exception tells Flask to show. For example, here are some custom JSON exception classes from a project of mine: class JSONException(HTTPException):
links = None
def __init__(self, links=None, *args, **kwargs):
super(JSONException, self).__init__(*args, **kwargs)
self.links = links or {}
def get_links(self):
return {name: {'href': href} for name, href in self.links}
def get_headers(self, environ=None):
return [('Content-Type', 'application/vnd.error+json')]
def get_body(self, environ=None):
return {'message': self.description, '_links': self.get_links()}
def get_response(self, environ=None):
rv = json.dumps(self.get_body(environ), indent=2)
return current_app.response_class(rv, self.code, self.get_headers(environ))
class ValidationError(JSONException):
code = 400
description = _('Validation error')
def __init__(self, errors=[], *args, **kwargs):
super(ValidationError, self).__init__(*args, **kwargs)
self.errors = errors
def get_body(self, environ=None):
rv = super(ValidationError, self).get_body(environ)
if self.errors:
rv['_embedded'] = {'errors': self.errors}
rv['total'] = len(self.errors)
return rv
class SearchQueryError(ValidationError):
description = _('Invalid search query')
class AuthenticationError(JSONException):
def get_headers(self, environ=None):
return super(AuthenticationError, self).get_headers() + [
('WWW-Authenticate', 'JWT realm="Login Required"')
]
class Unauthenticated(AuthenticationError):
code = 401
description = _('Authentication required')
class Unauthorized(AuthenticationError):
code = 403
description = _('Unauthorized')
class ResourceNotFound(JSONException):
code = 404
description = _('Resource not found') No where in my app have I registered these exceptions to be handled by my application. I just raise them, and if it happens to be in the context of a request, Flask returns an appropriate response for me. |
abandoning for #1291 |
the current master says:
this will only work if you register a exception instance, not an exception class, which is definitely not what users expect. instead of:
you’d have to do
i think you meant
issubclass(code_or_exception, HTTPException)
, but to preserve backwards compatibility, we should change it to accept both