diff --git a/CHANGES b/CHANGES index 4f400d8e..ae9cbdb9 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,8 @@ Relase date to be decided, codename to be chosen. - Empty session cookies are now deleted properly automatically. - View functions can now opt out of getting the automatic OPTIONS implementation. +- HTTP exceptions and Bad Request Key Errors can now be trapped so that they + show up normally in the traceback. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index 2487e8b7..4995ee84 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -81,6 +81,23 @@ The following configuration values are used internally by Flask: reject incoming requests with a content length greater than this by returning a 413 status code. +``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will + not execute the error handlers of HTTP + exceptions but instead treat the + exception like any other and bubble it + through the exception stack. This is + helpful for hairy debugging situations + where you have to find out where an HTTP + exception is coming from. +``TRAP_BAD_REQUEST_KEY_ERRORS`` Werkzeug's internal data structures that + deal with request specific data will + raise special key errors that are also + bad request exceptions. By default + these will be converted into 400 + responses which however can make + debugging some issues harder. If this + config is set to ``True`` you will get + a regular traceback instead. ================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -114,6 +131,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.7 ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` +.. versionadded:: 0.8 + ``TRAP_BAD_REQUEST_KEY_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 5c448144..359a233d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,7 @@ from itertools import chain from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed + MethodNotAllowed, BadRequest from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ locked_cached_property, _tojson_filter, _endpoint_from_view_func @@ -197,7 +197,9 @@ class Flask(_PackageBoundObject): 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, - 'MAX_CONTENT_LENGTH': None + 'MAX_CONTENT_LENGTH': None, + 'TRAP_BAD_REQUEST_KEY_ERRORS': False, + 'TRAP_HTTP_EXCEPTIONS': False }) #: The rule object to use for URL rules created. This is used by @@ -983,6 +985,24 @@ class Flask(_PackageBoundObject): return e return handler(e) + def trap_http_exception(self, e): + """Checks if an HTTP exception should be trapped or not. By default + this will return `False` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_KEY_ERRORS`` is set to `True`. It + also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. + + This is called for all HTTP exceptions raised by a view function. + If it returns `True` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + """ + if self.config['TRAP_HTTP_EXCEPTIONS']: + return True + if self.config['TRAP_BAD_REQUEST_KEY_ERRORS']: + return isinstance(e, BadRequest) and isinstance(e, LookupError) + return False + def handle_user_exception(self, e): """This method is called whenever an exception occurs that should be handled. A special case are @@ -993,14 +1013,16 @@ class Flask(_PackageBoundObject): .. 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 + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. Of course + # we cannot prevent users from trashing it themselves in a custom + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + blueprint_handlers = () handlers = self.error_handler_spec.get(request.blueprint) if handlers is not None: diff --git a/tests/flask_tests.py b/tests/flask_tests.py index fb3e0e52..dbd38e12 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -23,7 +23,7 @@ from contextlib import contextmanager from functools import update_wrapper from datetime import datetime from werkzeug import parse_date, parse_options_header -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import NotFound, BadRequest from werkzeug.http import parse_set_header from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -592,6 +592,40 @@ class BasicFunctionalityTestCase(unittest.TestCase): c = app.test_client() assert c.get('/').data == '42' + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + assert c.get('/fail').status_code == 400 + + app.config['TRAP_BAD_REQUEST_KEY_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + assert isinstance(e, BadRequest) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + def test_teardown_on_pop(self): buffer = [] app = flask.Flask(__name__)