Skip to content

Commit 7b92b6b

Browse files
committed
asyncio: avoid sharing exception object between StreamReader and close waiter
StreamReaderProtocol.connection_lost() set the same exception object on both the StreamReader's waiter and the Stream's _closed waiter. Since gh-90082, Future stores the traceback at set_exception() time and restores it with with_traceback() on every result() call, which mutates the exception in place. Sharing one object between two futures caused the second await (typically writer.wait_closed() in an except block) to rewrite the traceback of the in-flight exception being handled, erasing the real failure site (readexactly) and replacing it with wait_closed frames. Fix by copying the exception for the _closed waiter so each future owns an independent object. Copy falls back to reconstructing via type(exc)(*exc.args) when copy.copy fails. Fixes #156278
1 parent 43a1869 commit 7b92b6b

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

Lib/asyncio/streams.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'open_connection', 'start_server')
44

55
import collections
6+
import copy
67
import socket
78
import sys
89
import warnings
@@ -280,7 +281,29 @@ def connection_lost(self, exc):
280281
if exc is None:
281282
self._closed.set_result(None)
282283
else:
283-
self._closed.set_exception(exc)
284+
# Avoid sharing the same exception object between the
285+
# reader future and the close waiter. Future.result()
286+
# restores the traceback with `with_traceback()`, which
287+
# mutates the exception in place; sharing one object
288+
# between two futures rewrites the traceback of the
289+
# in-flight exception being handled (gh-156278).
290+
try:
291+
exc_copy = copy.copy(exc)
292+
except Exception:
293+
try:
294+
exc_copy = type(exc)(*exc.args)
295+
# Preserve context attributes where possible.
296+
if hasattr(exc, "__cause__"):
297+
exc_copy.__cause__ = exc.__cause__
298+
if hasattr(exc, "__context__"):
299+
exc_copy.__context__ = exc.__context__
300+
if hasattr(exc, "__suppress_context__"):
301+
exc_copy.__suppress_context__ = exc.__suppress_context__
302+
if exc.__traceback__ is not None:
303+
exc_copy = exc_copy.with_traceback(exc.__traceback__)
304+
except Exception:
305+
exc_copy = exc
306+
self._closed.set_exception(exc_copy)
284307
super().connection_lost(exc)
285308
self._stream_reader_wr = None
286309
self._task = None

0 commit comments

Comments
 (0)