forked from orbit-oss/flask
Merge pull request #1291 from flying-sheep/errorhandler-rework
Fixed and intuitivized exception handling
This commit is contained in:
commit
aed464b92b
2 changed files with 182 additions and 30 deletions
99
flask/app.py
99
flask/app.py
|
|
@ -8,18 +8,18 @@
|
||||||
:copyright: (c) 2015 by Armin Ronacher.
|
:copyright: (c) 2015 by Armin Ronacher.
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
from collections import Mapping
|
||||||
|
|
||||||
from werkzeug.datastructures import ImmutableDict
|
from werkzeug.datastructures import ImmutableDict
|
||||||
from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
|
from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
|
||||||
from werkzeug.exceptions import HTTPException, InternalServerError, \
|
from werkzeug.exceptions import HTTPException, InternalServerError, \
|
||||||
MethodNotAllowed, BadRequest
|
MethodNotAllowed, BadRequest, default_exceptions
|
||||||
|
|
||||||
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
|
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
|
||||||
locked_cached_property, _endpoint_from_view_func, find_package
|
locked_cached_property, _endpoint_from_view_func, find_package
|
||||||
|
|
@ -33,7 +33,7 @@ from .templating import DispatchingJinjaLoader, Environment, \
|
||||||
_default_template_ctx_processor
|
_default_template_ctx_processor
|
||||||
from .signals import request_started, request_finished, got_request_exception, \
|
from .signals import request_started, request_finished, got_request_exception, \
|
||||||
request_tearing_down, appcontext_tearing_down
|
request_tearing_down, appcontext_tearing_down
|
||||||
from ._compat import reraise, string_types, text_type, integer_types
|
from ._compat import reraise, string_types, text_type, integer_types, iterkeys
|
||||||
|
|
||||||
# a lock used for logger initialization
|
# a lock used for logger initialization
|
||||||
_logger_lock = Lock()
|
_logger_lock = Lock()
|
||||||
|
|
@ -1078,6 +1078,21 @@ class Flask(_PackageBoundObject):
|
||||||
return f
|
return f
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_exc_class_and_code(exc_class_or_code):
|
||||||
|
"""Ensure that we register only exceptions as handler keys"""
|
||||||
|
if isinstance(exc_class_or_code, integer_types):
|
||||||
|
exc_class = default_exceptions[exc_class_or_code]
|
||||||
|
else:
|
||||||
|
exc_class = exc_class_or_code
|
||||||
|
|
||||||
|
assert issubclass(exc_class, Exception)
|
||||||
|
|
||||||
|
if issubclass(exc_class, HTTPException):
|
||||||
|
return exc_class, exc_class.code
|
||||||
|
else:
|
||||||
|
return exc_class, None
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def errorhandler(self, code_or_exception):
|
def errorhandler(self, code_or_exception):
|
||||||
"""A decorator that is used to register a function give a given
|
"""A decorator that is used to register a function give a given
|
||||||
|
|
@ -1136,16 +1151,21 @@ class Flask(_PackageBoundObject):
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def _register_error_handler(self, key, code_or_exception, f):
|
def _register_error_handler(self, key, code_or_exception, f):
|
||||||
if isinstance(code_or_exception, HTTPException):
|
"""
|
||||||
code_or_exception = code_or_exception.code
|
:type key: None|str
|
||||||
if isinstance(code_or_exception, integer_types):
|
:type code_or_exception: int|T<=Exception
|
||||||
assert code_or_exception != 500 or key is None, \
|
:type f: callable
|
||||||
'It is currently not possible to register a 500 internal ' \
|
"""
|
||||||
'server error on a per-blueprint level.'
|
if isinstance(code_or_exception, HTTPException): # old broken behavior
|
||||||
self.error_handler_spec.setdefault(key, {})[code_or_exception] = f
|
raise ValueError(
|
||||||
else:
|
'Tried to register a handler for an exception instance {0!r}. '
|
||||||
self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \
|
'Handlers can only be registered for exception classes or HTTP error codes.'
|
||||||
.append((code_or_exception, f))
|
.format(code_or_exception))
|
||||||
|
|
||||||
|
exc_class, code = self._get_exc_class_and_code(code_or_exception)
|
||||||
|
|
||||||
|
handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {})
|
||||||
|
handlers[exc_class] = f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def template_filter(self, name=None):
|
def template_filter(self, name=None):
|
||||||
|
|
@ -1386,6 +1406,33 @@ class Flask(_PackageBoundObject):
|
||||||
self.url_default_functions.setdefault(None, []).append(f)
|
self.url_default_functions.setdefault(None, []).append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
def _find_error_handler(self, e):
|
||||||
|
"""Finds a registered error handler for the request’s blueprint.
|
||||||
|
If neither blueprint nor App has a suitable handler registered, returns None
|
||||||
|
"""
|
||||||
|
exc_class, code = self._get_exc_class_and_code(type(e))
|
||||||
|
|
||||||
|
def find_superclass(handler_map):
|
||||||
|
if not handler_map:
|
||||||
|
return None
|
||||||
|
for superclass in exc_class.__mro__:
|
||||||
|
if superclass is BaseException:
|
||||||
|
return None
|
||||||
|
handler = handler_map.get(superclass)
|
||||||
|
if handler is not None:
|
||||||
|
handler_map[exc_class] = handler # cache for next time exc_class is raised
|
||||||
|
return handler
|
||||||
|
return None
|
||||||
|
|
||||||
|
# try blueprint handlers
|
||||||
|
handler = find_superclass(self.error_handler_spec.get(request.blueprint, {}).get(code))
|
||||||
|
|
||||||
|
if handler is not None:
|
||||||
|
return handler
|
||||||
|
|
||||||
|
# fall back to app handlers
|
||||||
|
return find_superclass(self.error_handler_spec[None].get(code))
|
||||||
|
|
||||||
def handle_http_exception(self, e):
|
def handle_http_exception(self, e):
|
||||||
"""Handles an HTTP exception. By default this will invoke the
|
"""Handles an HTTP exception. By default this will invoke the
|
||||||
registered error handlers and fall back to returning the
|
registered error handlers and fall back to returning the
|
||||||
|
|
@ -1393,15 +1440,12 @@ class Flask(_PackageBoundObject):
|
||||||
|
|
||||||
.. versionadded:: 0.3
|
.. versionadded:: 0.3
|
||||||
"""
|
"""
|
||||||
handlers = self.error_handler_spec.get(request.blueprint)
|
|
||||||
# Proxy exceptions don't have error codes. We want to always return
|
# Proxy exceptions don't have error codes. We want to always return
|
||||||
# those unchanged as errors
|
# those unchanged as errors
|
||||||
if e.code is None:
|
if e.code is None:
|
||||||
return e
|
return e
|
||||||
if handlers and e.code in handlers:
|
|
||||||
handler = handlers[e.code]
|
handler = self._find_error_handler(e)
|
||||||
else:
|
|
||||||
handler = self.error_handler_spec[None].get(e.code)
|
|
||||||
if handler is None:
|
if handler is None:
|
||||||
return e
|
return e
|
||||||
return handler(e)
|
return handler(e)
|
||||||
|
|
@ -1443,20 +1487,15 @@ class Flask(_PackageBoundObject):
|
||||||
# wants the traceback preserved in handle_http_exception. Of course
|
# wants the traceback preserved in handle_http_exception. Of course
|
||||||
# we cannot prevent users from trashing it themselves in a custom
|
# we cannot prevent users from trashing it themselves in a custom
|
||||||
# trap_http_exception method so that's their fault then.
|
# trap_http_exception method so that's their fault then.
|
||||||
|
|
||||||
blueprint_handlers = ()
|
|
||||||
handlers = self.error_handler_spec.get(request.blueprint)
|
|
||||||
if handlers is not None:
|
|
||||||
blueprint_handlers = handlers.get(None, ())
|
|
||||||
app_handlers = self.error_handler_spec[None].get(None, ())
|
|
||||||
for typecheck, handler in chain(blueprint_handlers, app_handlers):
|
|
||||||
if isinstance(e, typecheck):
|
|
||||||
return handler(e)
|
|
||||||
|
|
||||||
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
|
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
|
||||||
return self.handle_http_exception(e)
|
return self.handle_http_exception(e)
|
||||||
|
|
||||||
reraise(exc_type, exc_value, tb)
|
handler = self._find_error_handler(e)
|
||||||
|
|
||||||
|
if handler is None:
|
||||||
|
reraise(exc_type, exc_value, tb)
|
||||||
|
return handler(e)
|
||||||
|
|
||||||
def handle_exception(self, e):
|
def handle_exception(self, e):
|
||||||
"""Default exception handling that kicks in when an exception
|
"""Default exception handling that kicks in when an exception
|
||||||
|
|
@ -1470,7 +1509,7 @@ class Flask(_PackageBoundObject):
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
exc_type, exc_value, tb = sys.exc_info()
|
||||||
|
|
||||||
got_request_exception.send(self, exception=e)
|
got_request_exception.send(self, exception=e)
|
||||||
handler = self.error_handler_spec[None].get(500)
|
handler = self._find_error_handler(InternalServerError())
|
||||||
|
|
||||||
if self.propagate_exceptions:
|
if self.propagate_exceptions:
|
||||||
# if we want to repropagate the exception, we can attempt to
|
# if we want to repropagate the exception, we can attempt to
|
||||||
|
|
|
||||||
113
tests/test_user_error_handler.py
Normal file
113
tests/test_user_error_handler.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from werkzeug.exceptions import Forbidden, InternalServerError
|
||||||
|
import flask
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_subclass():
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
|
class ParentException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChildExceptionUnregistered(ParentException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChildExceptionRegistered(ParentException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.errorhandler(ParentException)
|
||||||
|
def parent_exception_handler(e):
|
||||||
|
assert isinstance(e, ParentException)
|
||||||
|
return 'parent'
|
||||||
|
|
||||||
|
@app.errorhandler(ChildExceptionRegistered)
|
||||||
|
def child_exception_handler(e):
|
||||||
|
assert isinstance(e, ChildExceptionRegistered)
|
||||||
|
return 'child-registered'
|
||||||
|
|
||||||
|
@app.route('/parent')
|
||||||
|
def parent_test():
|
||||||
|
raise ParentException()
|
||||||
|
|
||||||
|
@app.route('/child-unregistered')
|
||||||
|
def unregistered_test():
|
||||||
|
raise ChildExceptionUnregistered()
|
||||||
|
|
||||||
|
@app.route('/child-registered')
|
||||||
|
def registered_test():
|
||||||
|
raise ChildExceptionRegistered()
|
||||||
|
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
|
||||||
|
assert c.get('/parent').data == b'parent'
|
||||||
|
assert c.get('/child-unregistered').data == b'parent'
|
||||||
|
assert c.get('/child-registered').data == b'child-registered'
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_http_subclass():
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
|
class ForbiddenSubclassRegistered(Forbidden):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ForbiddenSubclassUnregistered(Forbidden):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def code_exception_handler(e):
|
||||||
|
assert isinstance(e, Forbidden)
|
||||||
|
return 'forbidden'
|
||||||
|
|
||||||
|
@app.errorhandler(ForbiddenSubclassRegistered)
|
||||||
|
def subclass_exception_handler(e):
|
||||||
|
assert isinstance(e, ForbiddenSubclassRegistered)
|
||||||
|
return 'forbidden-registered'
|
||||||
|
|
||||||
|
@app.route('/forbidden')
|
||||||
|
def forbidden_test():
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
@app.route('/forbidden-registered')
|
||||||
|
def registered_test():
|
||||||
|
raise ForbiddenSubclassRegistered()
|
||||||
|
|
||||||
|
@app.route('/forbidden-unregistered')
|
||||||
|
def unregistered_test():
|
||||||
|
raise ForbiddenSubclassUnregistered()
|
||||||
|
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
|
||||||
|
assert c.get('/forbidden').data == b'forbidden'
|
||||||
|
assert c.get('/forbidden-unregistered').data == b'forbidden'
|
||||||
|
assert c.get('/forbidden-registered').data == b'forbidden-registered'
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_blueprint():
|
||||||
|
bp = flask.Blueprint('bp', __name__)
|
||||||
|
|
||||||
|
@bp.errorhandler(500)
|
||||||
|
def bp_exception_handler(e):
|
||||||
|
return 'bp-error'
|
||||||
|
|
||||||
|
@bp.route('/error')
|
||||||
|
def bp_test():
|
||||||
|
raise InternalServerError()
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def app_exception_handler(e):
|
||||||
|
return 'app-error'
|
||||||
|
|
||||||
|
@app.route('/error')
|
||||||
|
def app_test():
|
||||||
|
raise InternalServerError()
|
||||||
|
|
||||||
|
app.register_blueprint(bp, url_prefix='/bp')
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
|
||||||
|
assert c.get('/error').data == b'app-error'
|
||||||
|
assert c.get('/bp/error').data == b'bp-error'
|
||||||
Loading…
Add table
Add a link
Reference in a new issue