mirror of https://github.com/pallets/flask.git
Added blueprint specific error handling
This commit is contained in:
parent
aaa24fc05a
commit
f5ec9952de
5
CHANGES
5
CHANGES
|
@ -51,6 +51,11 @@ Release date to be announced, codename to be selected
|
||||||
- Don't modify the session on :func:`flask.get_flashed_messages` if there
|
- Don't modify the session on :func:`flask.get_flashed_messages` if there
|
||||||
are no messages in the session.
|
are no messages in the session.
|
||||||
- `before_request` handlers are now able to abort requests with errors.
|
- `before_request` handlers are now able to abort requests with errors.
|
||||||
|
- it is not possible to define user exception handlers. That way you can
|
||||||
|
provide custom error messages from a central hub for certain errors that
|
||||||
|
might occur during request processing (for instance database connection
|
||||||
|
errors, timeouts from remote resources etc.).
|
||||||
|
- Blueprints can provide blueprint specific error handlers.
|
||||||
|
|
||||||
Version 0.6.1
|
Version 0.6.1
|
||||||
-------------
|
-------------
|
||||||
|
|
105
flask/app.py
105
flask/app.py
|
@ -234,12 +234,21 @@ class Flask(_PackageBoundObject):
|
||||||
#: To register a view function, use the :meth:`route` decorator.
|
#: To register a view function, use the :meth:`route` decorator.
|
||||||
self.view_functions = {}
|
self.view_functions = {}
|
||||||
|
|
||||||
#: A dictionary of all registered error handlers. The key is
|
# support for the now deprecated `error_handlers` attribute. The
|
||||||
#: be the error code as integer, the value the function that
|
# :attr:`error_handler_spec` shall be used now.
|
||||||
#: should handle that error.
|
self._error_handlers = {}
|
||||||
|
|
||||||
|
#: A dictionary of all registered error handlers. The key is `None`
|
||||||
|
#: for error handlers active on the application, otherwise the key is
|
||||||
|
#: the name of the blueprint. Each key points to another dictionary
|
||||||
|
#: where they key is the status code of the http exception. The
|
||||||
|
#: special key `None` points to a list of tuples where the first item
|
||||||
|
#: is the class for the instance check and the second the error handler
|
||||||
|
#: function.
|
||||||
|
#:
|
||||||
#: To register a error handler, use the :meth:`errorhandler`
|
#: To register a error handler, use the :meth:`errorhandler`
|
||||||
#: decorator.
|
#: decorator.
|
||||||
self.error_handlers = {}
|
self.error_handler_spec = {None: self._error_handlers}
|
||||||
|
|
||||||
#: A dictionary with lists of functions that should be called at the
|
#: A dictionary with lists of functions that should be called at the
|
||||||
#: beginning of the request. The key of the dictionary is the name of
|
#: beginning of the request. The key of the dictionary is the name of
|
||||||
|
@ -351,6 +360,17 @@ class Flask(_PackageBoundObject):
|
||||||
endpoint='static',
|
endpoint='static',
|
||||||
view_func=self.send_static_file)
|
view_func=self.send_static_file)
|
||||||
|
|
||||||
|
def _get_error_handlers(self):
|
||||||
|
from warnings import warn
|
||||||
|
warn(DeprecationWarning('error_handlers is deprecated, use the '
|
||||||
|
'new error_handler_spec attribute instead.'), stacklevel=1)
|
||||||
|
return self._error_handlers
|
||||||
|
def _set_error_handlers(self, value):
|
||||||
|
self._error_handlers = value
|
||||||
|
self.error_handler_spec[None] = value
|
||||||
|
error_handlers = property(_get_error_handlers, _set_error_handlers)
|
||||||
|
del _get_error_handlers, _set_error_handlers
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def propagate_exceptions(self):
|
def propagate_exceptions(self):
|
||||||
"""Returns the value of the `PROPAGATE_EXCEPTIONS` configuration
|
"""Returns the value of the `PROPAGATE_EXCEPTIONS` configuration
|
||||||
|
@ -761,7 +781,7 @@ class Flask(_PackageBoundObject):
|
||||||
return f
|
return f
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def errorhandler(self, code):
|
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
|
||||||
error code. Example::
|
error code. Example::
|
||||||
|
|
||||||
|
@ -769,21 +789,51 @@ class Flask(_PackageBoundObject):
|
||||||
def page_not_found(error):
|
def page_not_found(error):
|
||||||
return 'This page does not exist', 404
|
return 'This page does not exist', 404
|
||||||
|
|
||||||
|
You can also register handlers for arbitrary exceptions::
|
||||||
|
|
||||||
|
@app.errorhandler(DatabaseError)
|
||||||
|
def special_exception_handler(error):
|
||||||
|
return 'Database connection failed', 500
|
||||||
|
|
||||||
You can also register a function as error handler without using
|
You can also register a function as error handler without using
|
||||||
the :meth:`errorhandler` decorator. The following example is
|
the :meth:`errorhandler` decorator. The following example is
|
||||||
equivalent to the one above::
|
equivalent to the one above::
|
||||||
|
|
||||||
def page_not_found(error):
|
def page_not_found(error):
|
||||||
return 'This page does not exist', 404
|
return 'This page does not exist', 404
|
||||||
app.error_handlers[404] = page_not_found
|
app.error_handler_spec[None][404] = page_not_found
|
||||||
|
|
||||||
|
Setting error handlers via assignments to :attr:`error_handler_spec`
|
||||||
|
however is discouraged as it requires fidling with nested dictionaries
|
||||||
|
and the special case for arbitrary exception types.
|
||||||
|
|
||||||
|
The first `None` refers to the active blueprint. If the error
|
||||||
|
handler should be application wide `None` shall be used.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
One can now additionally also register custom exception types
|
||||||
|
that do not necessarily have to be a subclass of the
|
||||||
|
:class:~`werkzeug.exceptions.HTTPException` class.
|
||||||
|
|
||||||
:param code: the code as integer for the handler
|
:param code: the code as integer for the handler
|
||||||
"""
|
"""
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
self.error_handlers[code] = f
|
self._register_error_handler(None, code_or_exception, f)
|
||||||
return f
|
return f
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def _register_error_handler(self, key, code_or_exception, f):
|
||||||
|
if isinstance(code_or_exception, HTTPException):
|
||||||
|
code_or_exception = code_or_exception.code
|
||||||
|
if isinstance(code_or_exception, (int, long)):
|
||||||
|
assert code_or_exception != 500 or key is None, \
|
||||||
|
'It is currently not possible to register a 500 internal ' \
|
||||||
|
'server error on a per-blueprint level.'
|
||||||
|
self.error_handler_spec.setdefault(key, {})[code_or_exception] = f
|
||||||
|
else:
|
||||||
|
self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \
|
||||||
|
.append((code_or_exception, f))
|
||||||
|
|
||||||
def template_filter(self, name=None):
|
def template_filter(self, name=None):
|
||||||
"""A decorator that is used to register custom template filter.
|
"""A decorator that is used to register custom template filter.
|
||||||
You can specify a name for the filter, otherwise the function
|
You can specify a name for the filter, otherwise the function
|
||||||
|
@ -871,11 +921,44 @@ class Flask(_PackageBoundObject):
|
||||||
|
|
||||||
.. versionadded: 0.3
|
.. versionadded: 0.3
|
||||||
"""
|
"""
|
||||||
handler = self.error_handlers.get(e.code)
|
handlers = self.error_handler_spec.get(request.blueprint)
|
||||||
|
if handlers and e.code in handlers:
|
||||||
|
handler = handlers[e.code]
|
||||||
|
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)
|
||||||
|
|
||||||
|
def handle_user_exception(self, e):
|
||||||
|
"""This method is called whenever an exception occurs that should be
|
||||||
|
handled. A special case are
|
||||||
|
:class:`~werkzeug.exception.HTTPException`\s which are forwarded by
|
||||||
|
this function to the :meth:`handle_http_exception` method. This
|
||||||
|
function will either return a response value or reraise the
|
||||||
|
exception with the same traceback.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
"""
|
||||||
|
# ensure not to trash sys.exc_info() at that point in case someone
|
||||||
|
# wants the traceback preserved in handle_http_exception.
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
return self.handle_http_exception(e)
|
||||||
|
|
||||||
|
exc_type, exc_value, tb = sys.exc_info()
|
||||||
|
assert exc_value is e
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
raise exc_type, exc_value, tb
|
||||||
|
|
||||||
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
|
||||||
occours that is not caught. In debug mode the exception will
|
occours that is not caught. In debug mode the exception will
|
||||||
|
@ -888,7 +971,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_handlers.get(500)
|
handler = self.error_handler_spec[None].get(500)
|
||||||
|
|
||||||
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
|
||||||
|
@ -942,8 +1025,8 @@ class Flask(_PackageBoundObject):
|
||||||
rv = self.preprocess_request()
|
rv = self.preprocess_request()
|
||||||
if rv is None:
|
if rv is None:
|
||||||
rv = self.dispatch_request()
|
rv = self.dispatch_request()
|
||||||
except HTTPException, e:
|
except Exception, e:
|
||||||
rv = self.handle_http_exception(e)
|
rv = self.handle_user_exception(e)
|
||||||
response = self.make_response(rv)
|
response = self.make_response(rv)
|
||||||
response = self.process_response(response)
|
response = self.process_response(response)
|
||||||
request_finished.send(self, response=response)
|
request_finished.send(self, response=response)
|
||||||
|
|
|
@ -62,6 +62,7 @@ class Blueprint(_PackageBoundObject):
|
||||||
self.static_folder = static_folder
|
self.static_folder = static_folder
|
||||||
self.static_url_path = static_url_path
|
self.static_url_path = static_url_path
|
||||||
self.deferred_functions = []
|
self.deferred_functions = []
|
||||||
|
self.view_functions = {}
|
||||||
|
|
||||||
def _record(self, func):
|
def _record(self, func):
|
||||||
self.deferred_functions.append(func)
|
self.deferred_functions.append(func)
|
||||||
|
@ -110,7 +111,9 @@ class Blueprint(_PackageBoundObject):
|
||||||
def endpoint(self, endpoint):
|
def endpoint(self, endpoint):
|
||||||
"""Like :meth:`Flask.endpoint` but for a module. This does not
|
"""Like :meth:`Flask.endpoint` but for a module. This does not
|
||||||
prefix the endpoint with the module name, this has to be done
|
prefix the endpoint with the module name, this has to be done
|
||||||
explicitly by the user of this method.
|
explicitly by the user of this method. If the endpoint is prefixed
|
||||||
|
with a `.` it will be registered to the current blueprint, otherwise
|
||||||
|
it's an application independent endpoint.
|
||||||
"""
|
"""
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
def register_endpoint(state):
|
def register_endpoint(state):
|
||||||
|
@ -209,3 +212,22 @@ class Blueprint(_PackageBoundObject):
|
||||||
self._record_once(lambda s: s.app.url_default_functions
|
self._record_once(lambda s: s.app.url_default_functions
|
||||||
.setdefault(None, []).append(f))
|
.setdefault(None, []).append(f))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
def errorhandler(self, code_or_exception):
|
||||||
|
"""Registers an error handler that becomes active for this blueprint
|
||||||
|
only. Please be aware that routing does not happen local to a
|
||||||
|
blueprint so an error handler for 404 usually is not handled by
|
||||||
|
a blueprint unless it is caused inside a view function. Another
|
||||||
|
special case is the 500 internal server error which is always looked
|
||||||
|
up from the application.
|
||||||
|
|
||||||
|
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator
|
||||||
|
of the :class:`~flask.Flask` object.
|
||||||
|
|
||||||
|
.. versionadded:: 0.7
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
self._record_once(lambda s: s.app._register_error_handler(
|
||||||
|
self.name, code_or_exception, f))
|
||||||
|
return f
|
||||||
|
return decorator
|
||||||
|
|
|
@ -531,6 +531,22 @@ class BasicFunctionalityTestCase(unittest.TestCase):
|
||||||
assert rv.status_code == 500
|
assert rv.status_code == 500
|
||||||
assert 'internal server error' == rv.data
|
assert 'internal server error' == rv.data
|
||||||
|
|
||||||
|
def test_user_error_handling(self):
|
||||||
|
class MyException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
@app.errorhandler(MyException)
|
||||||
|
def handle_my_exception(e):
|
||||||
|
assert isinstance(e, MyException)
|
||||||
|
return '42'
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
raise MyException()
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
assert c.get('/').data == '42'
|
||||||
|
|
||||||
def test_teardown_on_pop(self):
|
def test_teardown_on_pop(self):
|
||||||
buffer = []
|
buffer = []
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
|
@ -1214,6 +1230,49 @@ class ModuleTestCase(unittest.TestCase):
|
||||||
assert c.get('/foo/bar').data == 'bar'
|
assert c.get('/foo/bar').data == 'bar'
|
||||||
|
|
||||||
|
|
||||||
|
class BlueprintTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_blueprint_specific_error_handling(self):
|
||||||
|
frontend = flask.Blueprint('frontend', __name__)
|
||||||
|
backend = flask.Blueprint('backend', __name__)
|
||||||
|
sideend = flask.Blueprint('sideend', __name__)
|
||||||
|
|
||||||
|
@frontend.errorhandler(403)
|
||||||
|
def frontend_forbidden(e):
|
||||||
|
return 'frontend says no', 403
|
||||||
|
|
||||||
|
@frontend.route('/frontend-no')
|
||||||
|
def frontend_no():
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
|
@backend.errorhandler(403)
|
||||||
|
def backend_forbidden(e):
|
||||||
|
return 'backend says no', 403
|
||||||
|
|
||||||
|
@backend.route('/backend-no')
|
||||||
|
def backend_no():
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
|
@sideend.route('/what-is-a-sideend')
|
||||||
|
def sideend_no():
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.register_blueprint(frontend)
|
||||||
|
app.register_blueprint(backend)
|
||||||
|
app.register_blueprint(sideend)
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def app_forbidden(e):
|
||||||
|
return 'application itself says no', 403
|
||||||
|
|
||||||
|
c = app.test_client()
|
||||||
|
|
||||||
|
assert c.get('/frontend-no').data == 'frontend says no'
|
||||||
|
assert c.get('/backend-no').data == 'backend says no'
|
||||||
|
assert c.get('/what-is-a-sideend').data == 'application itself says no'
|
||||||
|
|
||||||
|
|
||||||
class SendfileTestCase(unittest.TestCase):
|
class SendfileTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def test_send_file_regular(self):
|
def test_send_file_regular(self):
|
||||||
|
@ -1631,6 +1690,7 @@ def suite():
|
||||||
suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase))
|
suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase))
|
||||||
suite.addTest(unittest.makeSuite(TemplatingTestCase))
|
suite.addTest(unittest.makeSuite(TemplatingTestCase))
|
||||||
suite.addTest(unittest.makeSuite(ModuleTestCase))
|
suite.addTest(unittest.makeSuite(ModuleTestCase))
|
||||||
|
suite.addTest(unittest.makeSuite(BlueprintTestCase))
|
||||||
suite.addTest(unittest.makeSuite(SendfileTestCase))
|
suite.addTest(unittest.makeSuite(SendfileTestCase))
|
||||||
suite.addTest(unittest.makeSuite(LoggingTestCase))
|
suite.addTest(unittest.makeSuite(LoggingTestCase))
|
||||||
suite.addTest(unittest.makeSuite(ConfigTestCase))
|
suite.addTest(unittest.makeSuite(ConfigTestCase))
|
||||||
|
|
Loading…
Reference in New Issue