From b23e1d399ee94c8455f033ee342122d5a93459a5 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:10:11 -0400 Subject: [PATCH] fix: TLSSession guards unbuilt session in _is_closed/close; document setup() is not auto-invoked _is_closed() and close() called self.session unconditionally, routing through the lazy session property and building a real backend client even when the session was never used - including during __del__ on GC of a constructed-but-unused TLSSession, silently building (and leaking) a backend client nothing ever closes. Mirrors aioweb.ExtendedSession's own _session is None guards. Also corrects setup()'s docstring, which claimed TLSSession invokes a backend's setup() lazily before the first request - it never does; only Noble self-invokes it from its own raw_request. Signed-off-by: disqualifier --- src/aioweb_tls/session.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/aioweb_tls/session.py b/src/aioweb_tls/session.py index 1a6aefa..cda395d 100644 --- a/src/aioweb_tls/session.py +++ b/src/aioweb_tls/session.py @@ -38,8 +38,10 @@ class TLSSession(ExtendedSession): async def setup(self) -> None: """run the backend's one-time setup if it has one (idempotent, e.g. noble's Go lib fetch) - optional to call: also invoked lazily before the first request, but calling - it upfront lets callers pre-warm at startup. + optional to call upfront to pre-warm at startup. TLSSession itself never calls + this automatically - a backend needing lazy setup must self-invoke it from its + own raw_request, as Noble does; a custom backend that skips this will never run + setup() unless the caller calls TLSSession.setup() explicitly. """ setup = getattr(self.backend, "setup", None) if setup is not None: @@ -64,9 +66,14 @@ class TLSSession(ExtendedSession): return cookies_for_url(self.session, url) def _is_closed(self) -> bool: - """closed if explicitly closed here or the backend reports it""" + """closed if explicitly closed here, never built, or the backend reports it + + an unbuilt session counts as closed: nothing was opened, nothing to leak. + """ if self._closed: return True + if self._session is None: + return True return self.backend.is_closed(self.session) # ------------------------------------------------------------------------- @@ -104,10 +111,12 @@ class TLSSession(ExtendedSession): # lifecycle - backends close differently, so route through the backend async def close(self) -> None: - """close via the backend's close (falls back to session.close)""" + """close via the backend's close (falls back to session.close); no-op if never built""" self._closed = True + if self._session is None: + return close = getattr(self.backend, "close", None) if close is not None: - await close(self.session) + await close(self._session) else: - await self.session.close() + await self._session.close()