Fixed request context preservation and teardown handler interaction.

This commit is contained in:
Armin Ronacher 2013-06-02 21:47:28 +01:00
parent 6dfe933260
commit 1b40b3b573
4 changed files with 59 additions and 1 deletions

View file

@ -62,6 +62,9 @@ Release date to be decided.
- Changed how the teardown system is informed about exceptions. This is now - Changed how the teardown system is informed about exceptions. This is now
more reliable in case something handles an exception halfway through more reliable in case something handles an exception halfway through
the error handling process. 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. - Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable.
- Flask now orders JSON keys by default to not trash HTTP caches due to - Flask now orders JSON keys by default to not trash HTTP caches due to
different hash seeds between different workers. different hash seeds between different workers.

View file

@ -1707,6 +1707,13 @@ class Flask(_PackageBoundObject):
rv = func(exc) rv = func(exc)
request_tearing_down.send(self, exc=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): def do_teardown_appcontext(self, exc=None):
"""Called when an application context is popped. This works pretty """Called when an application context is popped. This works pretty
much the same as :meth:`do_teardown_request` but for the application much the same as :meth:`do_teardown_request` but for the application

View file

@ -235,6 +235,10 @@ class RequestContext(object):
# is pushed the preserved context is popped. # is pushed the preserved context is popped.
self.preserved = False 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 # Functions that should be executed after the request on the response
# object. These will be called before the regular "after_request" # object. These will be called before the regular "after_request"
# functions. # functions.
@ -296,7 +300,7 @@ class RequestContext(object):
# functionality is not active in production environments. # functionality is not active in production environments.
top = _request_ctx_stack.top top = _request_ctx_stack.top
if top is not None and top.preserved: 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 # Before we push the request context we have to ensure that there
# is an application context. # is an application context.
@ -331,9 +335,18 @@ class RequestContext(object):
clear_request = False clear_request = False
if not self._implicit_app_ctx_stack: if not self._implicit_app_ctx_stack:
self.preserved = False self.preserved = False
self._preserved_exc = None
if exc is None: if exc is None:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
self.app.do_teardown_request(exc) 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) request_close = getattr(self.request, 'close', None)
if request_close is not None: if request_close is not None:
request_close() request_close()
@ -356,6 +369,7 @@ class RequestContext(object):
if self.request.environ.get('flask._preserve_context') or \ if self.request.environ.get('flask._preserve_context') or \
(exc is not None and self.app.preserve_context_on_exception): (exc is not None and self.app.preserve_context_on_exception):
self.preserved = True self.preserved = True
self._preserved_exc = exc
else: else:
self.pop(exc) self.pop(exc)

View file

@ -1070,6 +1070,40 @@ class BasicFunctionalityTestCase(FlaskTestCase):
self.assert_true(flask._request_ctx_stack.top is None) self.assert_true(flask._request_ctx_stack.top is None)
self.assert_true(flask._app_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): class SubdomainTestCase(FlaskTestCase):