From 859d9a9d5c3da114132ec9f0e515e1fa8030f68f Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 31 May 2017 18:02:23 -0700 Subject: [PATCH] show nice message when registering error handler for unknown code clean up error handler docs closes #1837 --- docs/errorhandling.rst | 71 ++++++++++++++++++++++++++++-------------- flask/app.py | 55 +++++++++++++++----------------- tests/test_basic.py | 7 +++++ 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 2791fec3..84c649ce 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -76,49 +76,72 @@ Error handlers You might want to show custom error pages to the user when an error occurs. This can be done by registering error handlers. -Error handlers are normal :ref:`views` but instead of being registered for -routes, they are registered for exceptions that are raised while trying to -do something else. +An error handler is a normal view function that return a response, but instead +of being registered for a route, it is registered for an exception or HTTP +status code that would is raised while trying to handle a request. Registering ``````````` -Register error handlers using :meth:`~flask.Flask.errorhandler` or -:meth:`~flask.Flask.register_error_handler`:: +Register handlers by decorating a function with +:meth:`~flask.Flask.errorhandler`. Or use +:meth:`~flask.Flask.register_error_handler` to register the function later. +Remember to set the error code when returning the response. :: @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): - return 'bad request!' + return 'bad request!', 400 - app.register_error_handler(400, lambda e: 'bad request!') + # or, without the decorator + app.register_error_handler(400, handle_bad_request) -Those two ways are equivalent, but the first one is more clear and leaves -you with a function to call on your whim (and in tests). Note that :exc:`werkzeug.exceptions.HTTPException` subclasses like -:exc:`~werkzeug.exceptions.BadRequest` from the example and their HTTP codes -are interchangeable when handed to the registration methods or decorator -(``BadRequest.code == 400``). +:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable +when registering handlers. (``BadRequest.code == 400``) -You are however not limited to :exc:`~werkzeug.exceptions.HTTPException` -or HTTP status codes but can register a handler for every exception class you -like. +Non-standard HTTP codes cannot be registered by code because they are not known +by Werkzeug. Instead, define a subclass of +:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and +register and raise that exception class. :: -.. versionchanged:: 0.11 + class InsufficientStorage(werkzeug.exceptions.HTTPException): + code = 507 + description = 'Not enough storage space.' + + app.register_error_handler(InsuffcientStorage, handle_507) - Errorhandlers are now prioritized by specificity of the exception classes - they are registered for instead of the order they are registered in. + raise InsufficientStorage() + +Handlers can be registered for any exception class, not just +:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status +codes. Handlers can be registered for a specific class, or for all subclasses +of a parent class. Handling ```````` -Once an exception instance is raised, its class hierarchy is traversed, -and searched for in the exception classes for which handlers are registered. -The most specific handler is selected. +When an exception is caught by Flask while handling a request, it is first +looked up by code. If no handler is registered for the code, it is looked up +by its class hierarchy; the most specific handler is chosen. If no handler is +registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a +generic message about their code, while other exceptions are converted to a +generic 500 Internal Server Error. -E.g. if an instance of :exc:`ConnectionRefusedError` is raised, and a handler +For example, if an instance of :exc:`ConnectionRefusedError` is raised, and a handler is registered for :exc:`ConnectionError` and :exc:`ConnectionRefusedError`, -the more specific :exc:`ConnectionRefusedError` handler is called on the -exception instance, and its response is shown to the user. +the more specific :exc:`ConnectionRefusedError` handler is called with the +exception instance to generate the response. + +Handlers registered on the blueprint take precedence over those registered +globally on the application, assuming a blueprint is handling the request that +raises the exception. However, the blueprint cannot handle 404 routing errors +because the 404 occurs at the routing level before the blueprint can be +determined. + +.. versionchanged:: 0.11 + + Handlers are prioritized by specificity of the exception classes they are + registered for instead of the order they are registered in. Error Mails ----------- diff --git a/flask/app.py b/flask/app.py index 3c6d9f54..342dde86 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1166,7 +1166,9 @@ class Flask(_PackageBoundObject): @setupmethod def errorhandler(self, code_or_exception): - """A decorator that is used to register a function given an + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an error code. Example:: @app.errorhandler(404) @@ -1179,21 +1181,6 @@ class Flask(_PackageBoundObject): def special_exception_handler(error): return 'Database connection failed', 500 - You can also register a function as error handler without using - the :meth:`errorhandler` decorator. The following example is - equivalent to the one above:: - - def page_not_found(error): - return 'This page does not exist', 404 - 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 fiddling 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 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error @@ -1212,6 +1199,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def register_error_handler(self, code_or_exception, f): """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator @@ -1230,11 +1218,18 @@ class Flask(_PackageBoundObject): """ if isinstance(code_or_exception, HTTPException): # old broken behavior raise ValueError( - 'Tried to register a handler for an exception instance {0!r}. ' - 'Handlers can only be registered for exception classes or HTTP error codes.' - .format(code_or_exception)) + 'Tried to register a handler for an exception instance {0!r}.' + ' Handlers can only be registered for exception classes or' + ' HTTP error codes.'.format(code_or_exception) + ) - exc_class, code = self._get_exc_class_and_code(code_or_exception) + try: + exc_class, code = self._get_exc_class_and_code(code_or_exception) + except KeyError: + raise KeyError( + "'{0}' is not a recognized HTTP error code. Use a subclass of" + " HTTPException with that code instead.".format(code_or_exception) + ) handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) handlers[exc_class] = f @@ -1339,7 +1334,7 @@ class Flask(_PackageBoundObject): @setupmethod def before_request(self, f): """Registers a function to run before each request. - + For example, this can be used to open a database connection, or to load the logged in user from the session. @@ -1467,12 +1462,12 @@ class Flask(_PackageBoundObject): """Register a URL value preprocessor function for all view functions in the application. These functions will be called before the :meth:`before_request` functions. - + The function can modify the values captured from the matched url before they are passed to the view. For example, this can be used to pop a common language code value and place it in ``g`` rather than pass it to every view. - + The function is passed the endpoint name and values dict. The return value is ignored. """ @@ -1543,7 +1538,7 @@ class Flask(_PackageBoundObject): exception is not called and it shows up as regular exception in the traceback. This is helpful for debugging implicitly raised HTTP exceptions. - + .. versionchanged:: 1.0 Bad request errors are not trapped by default in debug mode. @@ -1783,10 +1778,10 @@ class Flask(_PackageBoundObject): ``str`` (``unicode`` in Python 2) A response object is created with the string encoded to UTF-8 as the body. - + ``bytes`` (``str`` in Python 2) A response object is created with the bytes as the body. - + ``tuple`` Either ``(body, status, headers)``, ``(body, status)``, or ``(body, headers)``, where ``body`` is any of the other types @@ -1795,13 +1790,13 @@ class Flask(_PackageBoundObject): tuples. If ``body`` is a :attr:`response_class` instance, ``status`` overwrites the exiting value and ``headers`` are extended. - + :attr:`response_class` The object is returned unchanged. - + other :class:`~werkzeug.wrappers.Response` class The object is coerced to :attr:`response_class`. - + :func:`callable` The function is called as a WSGI application. The result is used to create a response object. @@ -1938,7 +1933,7 @@ class Flask(_PackageBoundObject): :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` registered with the app and the blueprint. - + If any :meth:`before_request` handler returns a non-None value, the value is handled as if it was the return value from the view, and further request handling is stopped. diff --git a/tests/test_basic.py b/tests/test_basic.py index c494e8bd..a38428b2 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -870,6 +870,13 @@ def test_error_handling(app, client): assert b'forbidden' == rv.data +def test_error_handler_unknown_code(app): + with pytest.raises(KeyError) as exc_info: + app.register_error_handler(999, lambda e: ('999', 999)) + + assert 'Use a subclass' in exc_info.value.args[0] + + def test_error_handling_processing(app, client): app.config['LOGGER_HANDLER_POLICY'] = 'never' app.testing = False