From 85735023f5ae7e2e729c0e934cbb67ece62e3426 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 19:23:31 -0400 Subject: [PATCH] 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 --- src/aioweb/session.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/aioweb/session.py b/src/aioweb/session.py index d1c725f..8c3c189 100644 --- a/src/aioweb/session.py +++ b/src/aioweb/session.py @@ -245,7 +245,11 @@ class ExtendedSession: 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). + 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} 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 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[name] = value cookie[name]["path"] = path @@ -283,7 +296,13 @@ class ExtendedSession: jar.update_cookies(cookie, response_url=url) 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() def _cookies_for_url(self, url): @@ -366,9 +385,12 @@ class ExtendedSession: unmask the caller's real IP) - pass only one of them. """ 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") - 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) merged = {**self._default_headers, **(kwargs.get("headers") or {})}