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 <dev@disqualifier.me>
This commit is contained in:
2026-07-06 00:10:11 -04:00
parent d40be6928a
commit b23e1d399e
+15 -6
View File
@@ -38,8 +38,10 @@ class TLSSession(ExtendedSession):
async def setup(self) -> None: async def setup(self) -> None:
"""run the backend's one-time setup if it has one (idempotent, e.g. noble's Go lib fetch) """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 optional to call upfront to pre-warm at startup. TLSSession itself never calls
it upfront lets callers pre-warm at startup. 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) setup = getattr(self.backend, "setup", None)
if setup is not None: if setup is not None:
@@ -64,9 +66,14 @@ class TLSSession(ExtendedSession):
return cookies_for_url(self.session, url) return cookies_for_url(self.session, url)
def _is_closed(self) -> bool: 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: if self._closed:
return True return True
if self._session is None:
return True
return self.backend.is_closed(self.session) return self.backend.is_closed(self.session)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -104,10 +111,12 @@ class TLSSession(ExtendedSession):
# lifecycle - backends close differently, so route through the backend # lifecycle - backends close differently, so route through the backend
async def close(self) -> None: 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 self._closed = True
if self._session is None:
return
close = getattr(self.backend, "close", None) close = getattr(self.backend, "close", None)
if close is not None: if close is not None:
await close(self.session) await close(self._session)
else: else:
await self.session.close() await self._session.close()