From 7b92b6bc4ccfe78072efda776b988e284cbfff98 Mon Sep 17 00:00:00 2001 From: aryansk Date: Sun, 23 Aug 2026 18:43:54 +0530 Subject: [PATCH 1/2] 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 --- Lib/asyncio/streams.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index 954e132617ce42b..9988589d4f942a8 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -3,6 +3,7 @@ 'open_connection', 'start_server') import collections +import copy import socket import sys import warnings @@ -280,7 +281,29 @@ def connection_lost(self, exc): if exc is None: self._closed.set_result(None) else: - self._closed.set_exception(exc) + # Avoid sharing the same exception object between the + # reader future and the close waiter. Future.result() + # restores the traceback with `with_traceback()`, which + # mutates the exception in place; sharing one object + # between two futures rewrites the traceback of the + # in-flight exception being handled (gh-156278). + try: + exc_copy = copy.copy(exc) + except Exception: + try: + exc_copy = type(exc)(*exc.args) + # Preserve context attributes where possible. + if hasattr(exc, "__cause__"): + exc_copy.__cause__ = exc.__cause__ + if hasattr(exc, "__context__"): + exc_copy.__context__ = exc.__context__ + if hasattr(exc, "__suppress_context__"): + exc_copy.__suppress_context__ = exc.__suppress_context__ + if exc.__traceback__ is not None: + exc_copy = exc_copy.with_traceback(exc.__traceback__) + except Exception: + exc_copy = exc + self._closed.set_exception(exc_copy) super().connection_lost(exc) self._stream_reader_wr = None self._task = None From dd72b5d319632fb820fb95e5d5ecfdb4ffba17de Mon Sep 17 00:00:00 2001 From: aryansk Date: Sun, 23 Aug 2026 19:03:58 +0530 Subject: [PATCH 2/2] Add NEWS entry for gh-156278 --- .../Library/2026-08-23-13-30-00.gh-issue-156278.Z9l5a1.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-23-13-30-00.gh-issue-156278.Z9l5a1.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-23-13-30-00.gh-issue-156278.Z9l5a1.rst b/Misc/NEWS.d/next/Library/2026-08-23-13-30-00.gh-issue-156278.Z9l5a1.rst new file mode 100644 index 000000000000000..974644fd286549f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-23-13-30-00.gh-issue-156278.Z9l5a1.rst @@ -0,0 +1,7 @@ +Fix :func:`asyncio.StreamWriter.wait_closed` traceback rewriting. + +``StreamReaderProtocol.connection_lost`` no longer shares the same +exception object between the reader and the close waiter, which +previously caused ``await writer.wait_closed()`` in an ``except`` +block to rewrite the ``__traceback__`` of the in-flight exception +being handled.