In Python 3 every exception has an auto-attached __context__ exception if it was
raised while an existing exception was being handled. This is often a very
useful property and allows users to see the underlying cause of many problems.
However, when dealing with async code via coroutines this feature can suddenly
become a pretty big issue. Namely, if you generator.throw back into a coroutine
to raise an exception in the correct scope, and that coroutine raises an
exception at a later time you can have nested exceptions which shouldn't
logically be nested.
This patch resolves this issue in the coroutine code by moving the gen.throw
outside of the exception handler. By doing this the user-code scope is able to
later raise exceptions without worrying about inheriting prior exception
context.
self.future = None
try:
orig_stack_contexts = stack_context._state.contexts
+ exc_info = None
+
try:
value = future.result()
except Exception:
self.had_exception = True
- yielded = self.gen.throw(*sys.exc_info())
+ exc_info = sys.exc_info()
+
+ if exc_info is not None:
+ yielded = self.gen.throw(*exc_info)
+ exc_info = None
else:
yielded = self.gen.send(value)
+
if stack_context._state.contexts is not orig_stack_contexts:
self.gen.throw(
stack_context.StackContextInconsistentError(
yield gen.sleep(0.01)
self.finished = True
+ @skipBefore33
+ @gen_test
+ def test_py3_leak_exception_context(self):
+ class LeakedException(Exception):
+ pass
+
+ @gen.coroutine
+ def inner(iteration):
+ raise LeakedException(iteration)
+
+ try:
+ yield inner(1)
+ except LeakedException as e:
+ self.assertEqual(str(e), "1")
+ self.assertIsNone(e.__context__)
+
+ try:
+ yield inner(2)
+ except LeakedException as e:
+ self.assertEqual(str(e), "2")
+ self.assertIsNone(e.__context__)
+
+ self.finished = True
class GenSequenceHandler(RequestHandler):
@asynchronous