From 1b40b3b573f5d98cf9fbc453305cd535c0b2578d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 21:47:28 +0100 Subject: [PATCH] Fixed request context preservation and teardown handler interaction. --- CHANGES | 3 +++ flask/app.py | 7 +++++++ flask/ctx.py | 16 +++++++++++++++- flask/testsuite/basic.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 378488f6..5bd76b97 100644 --- a/CHANGES +++ b/CHANGES @@ -62,6 +62,9 @@ Release date to be decided. - Changed how the teardown system is informed about exceptions. This is now more reliable in case something handles an exception halfway through the error handling process. +- Request context preservation in debug mode now keeps the exception + information around which means that teardown handlers are able to + distinguish error from success cases. - Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. - Flask now orders JSON keys by default to not trash HTTP caches due to different hash seeds between different workers. diff --git a/flask/app.py b/flask/app.py index 7b286571..b52af9b2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1707,6 +1707,13 @@ class Flask(_PackageBoundObject): rv = func(exc) request_tearing_down.send(self, exc=exc) + # If this interpreter supports clearing the exception information + # we do that now. This will only go into effect on Python 2.x, + # on 3.x it disappears automatically at the end of the exception + # stack. + if hasattr(sys, 'exc_clear'): + sys.exc_clear() + def do_teardown_appcontext(self, exc=None): """Called when an application context is popped. This works pretty much the same as :meth:`do_teardown_request` but for the application diff --git a/flask/ctx.py b/flask/ctx.py index 5e1ee2e3..6ea3158f 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -235,6 +235,10 @@ class RequestContext(object): # is pushed the preserved context is popped. self.preserved = False + # remembers the exception for pop if there is one in case the context + # preservation kicks in. + self._preserved_exc = None + # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. @@ -296,7 +300,7 @@ class RequestContext(object): # functionality is not active in production environments. top = _request_ctx_stack.top if top is not None and top.preserved: - top.pop() + top.pop(top._preserved_exc) # Before we push the request context we have to ensure that there # is an application context. @@ -331,9 +335,18 @@ class RequestContext(object): clear_request = False if not self._implicit_app_ctx_stack: self.preserved = False + self._preserved_exc = None if exc is None: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) + + # If this interpreter supports clearing the exception information + # we do that now. This will only go into effect on Python 2.x, + # on 3.x it disappears automatically at the end of the exception + # stack. + if hasattr(sys, 'exc_clear'): + sys.exc_clear() + request_close = getattr(self.request, 'close', None) if request_close is not None: request_close() @@ -356,6 +369,7 @@ class RequestContext(object): if self.request.environ.get('flask._preserve_context') or \ (exc is not None and self.app.preserve_context_on_exception): self.preserved = True + self._preserved_exc = exc else: self.pop(exc) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index bd5b2760..50ffcef8 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -1070,6 +1070,40 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_true(flask._request_ctx_stack.top is None) self.assert_true(flask._app_ctx_stack.top is None) + def test_preserve_remembers_exception(self): + app = flask.Flask(__name__) + app.debug = True + errors = [] + + @app.route('/fail') + def fail_func(): + 1 // 0 + + @app.route('/success') + def success_func(): + return 'Okay' + + @app.teardown_request + def teardown_handler(exc): + errors.append(exc) + + c = app.test_client() + + # After this failure we did not yet call the teardown handler + with self.assert_raises(ZeroDivisionError): + c.get('/fail') + self.assert_equal(errors, []) + + # But this request triggers it, and it's an error + c.get('/success') + self.assert_equal(len(errors), 2) + self.assert_true(isinstance(errors[0], ZeroDivisionError)) + + # At this point another request does nothing. + c.get('/success') + self.assert_equal(len(errors), 3) + self.assert_equal(errors[1], None) + class SubdomainTestCase(FlaskTestCase):