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:
"""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()