fix: proxy=None no longer drops an explicit proxies= (IP unmask); cookie views work off-loop

request(proxy=None, proxies={...}) silently sent direct because setdefault('proxy', ...)
is a no-op when the None key already exists, unmasking the caller's real IP - now a bare
proxy=None is replaced by the resolved proxies= (the both-set guard still rejects two real
proxies). get_cookies/clear_cookies return {}/no-op when no session was built yet (mirroring
preview) instead of forcing a build that needs a running loop; set_cookie, which genuinely
needs the jar, now raises a clear actionable error off-loop instead of aiohttp's opaque one.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-06 19:23:31 -04:00
parent 51c421c9a2
commit 85735023f5
+25 -3
View File
@@ -245,7 +245,11 @@ class ExtendedSession:
iterates the jar directly rather than filter_cookies() (which needs a url and, iterates the jar directly rather than filter_cookies() (which needs a url and,
given none, returns only domain-less shared cookies - {} for any normal one). given none, returns only domain-less shared cookies - {} for any normal one).
returns {} off-loop when no session was built yet (nothing could be set), mirroring
preview() - reaching self.session would build it and need a running loop.
""" """
if self._session is None:
return {}
return {c.key: c.value for c in self.session.cookie_jar} return {c.key: c.value for c in self.session.cookie_jar}
def set_cookie(self, name, value, domain=None, path="/"): def set_cookie(self, name, value, domain=None, path="/"):
@@ -262,6 +266,15 @@ class ExtendedSession:
to IP hosts with no store, no send, and no log - construct the session to IP hosts with no store, no send, and no log - construct the session
with cookie_jar=aiohttp.CookieJar(unsafe=True) to allow IP-host cookies with cookie_jar=aiohttp.CookieJar(unsafe=True) to allow IP-host cookies
""" """
if self._session is None:
try:
self._ensure_session()
except RuntimeError as error:
raise RuntimeError(
"set_cookie needs the backend cookie jar, which builds on first use and "
"requires a running event loop; call it inside an async context (unlike "
"get_cookies/clear_cookies, which no-op off-loop)"
) from error
cookie = SimpleCookie() cookie = SimpleCookie()
cookie[name] = value cookie[name] = value
cookie[name]["path"] = path cookie[name]["path"] = path
@@ -283,7 +296,13 @@ class ExtendedSession:
jar.update_cookies(cookie, response_url=url) jar.update_cookies(cookie, response_url=url)
def clear_cookies(self): def clear_cookies(self):
"""clear the session cookie jar""" """clear the session cookie jar
a no-op off-loop when no session was built yet (an unbuilt jar is already empty),
so callers can clear before the loop starts without a spurious session build.
"""
if self._session is None:
return
self.session.cookie_jar.clear() self.session.cookie_jar.clear()
def _cookies_for_url(self, url): def _cookies_for_url(self, url):
@@ -366,9 +385,12 @@ class ExtendedSession:
unmask the caller's real IP) - pass only one of them. unmask the caller's real IP) - pass only one of them.
""" """
resolved_proxy = self._get_proxy(url, kwargs.pop("proxies", None)) resolved_proxy = self._get_proxy(url, kwargs.pop("proxies", None))
if "proxy" in kwargs and kwargs["proxy"] is not None and resolved_proxy is not None: if kwargs.get("proxy") is not None and resolved_proxy is not None:
raise ValueError("pass only one of proxy= or proxies= (session/per-call), not both") raise ValueError("pass only one of proxy= or proxies= (session/per-call), not both")
kwargs.setdefault("proxy", resolved_proxy) # a bare proxy=None must not shadow a resolved proxies= (that would send direct and
# unmask the caller's IP); setdefault can't fix it because the None key already exists
if kwargs.get("proxy") is None:
kwargs["proxy"] = resolved_proxy
debug = kwargs.pop("debug", False) debug = kwargs.pop("debug", False)
merged = {**self._default_headers, **(kwargs.get("headers") or {})} merged = {**self._default_headers, **(kwargs.get("headers") or {})}