fix: preview() works without a running loop; set_cookie honors path when domain=None

preview()'s default cookie lookup reached the backend session and could force-
build it, raising RuntimeError('no running event loop') off-loop and defeating
the v0.1.7 lazy-session pre-loop use case; it's now skipped when the session
isn't built yet. set_cookie(domain=None) returned before setting the morsel's
path, so a shared cookie always stored path='/' regardless of the path= arg;
the morsel path is now set before that early return.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 16:17:46 -04:00
parent 27f0e49341
commit 7f73f21c93
3 changed files with 38 additions and 12 deletions
+29 -7
View File
@@ -11,18 +11,18 @@ and swap the HTTP client while inheriting everything else.
`requirements.txt`: `requirements.txt`:
``` ```
aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.10 aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.11
``` ```
Direct: Direct:
```bash ```bash
pip install "aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.10" pip install "aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.11"
``` ```
Requires `aiohttp` and `yarl` (pulled transitively). Requires `aiohttp` and `yarl` (pulled transitively).
Drop the `@v0.1.10` suffix from the line above to install the latest unpinned. Drop the `@v0.1.11` suffix from the line above to install the latest unpinned.
## Usage ## Usage
@@ -97,10 +97,15 @@ s.overwrite_domain("internal.local", "127.0.0.1") # host-substring rewrite
print(s.preview("POST", url, json={"a": 1}).as_curl()) # equivalent cURL command print(s.preview("POST", url, json={"a": 1}).as_curl()) # equivalent cURL command
``` ```
`as_curl()` renders `params` (merged into the url's query string) and `timeout` (as `preview()` is synchronous and never touches the backend session — it works before any
`--max-time`) as well as headers/body/proxy, so the emitted command is faithful to what event loop is running (e.g. building a preview at import/setup time), not just inside
`request()` actually sends. Pass `debug=True` to `request_with_retries` to log the cURL `asyncio.run()`. Its default cookie lookup only reads cookies from an already-built
preview and request flow. session; if the session hasn't been built yet there are no cookies to read, so it
returns none by default (pass `cookies=` explicitly to preview cookies for a session
that hasn't sent a request yet). `as_curl()` renders `params` (merged into the url's
query string) and `timeout` (as `--max-time`) as well as headers/body/proxy, so the
emitted command is faithful to what `request()` actually sends. Pass `debug=True` to
`request_with_retries` to log the cURL preview and request flow.
## Custom backends ## Custom backends
@@ -144,6 +149,23 @@ Two changes can't be shimmed without re-introducing the bugs they fix:
## Changelog ## Changelog
### v0.1.11
- **`preview()` no longer requires a running event loop.** Its default cookie
lookup (`cookies=` not passed) previously called `_cookies_for_url()`, which
reached the backend session and built it if absent — under aiohttp>=3.14 that
needs a running loop, so `preview()` raised `RuntimeError('no running event
loop')` when called before the loop starts, defeating its own pre-loop
build/inspect use case (v0.1.7). Now the default cookie lookup is skipped
entirely when the session hasn't been built yet (nothing could have been set
on a session that doesn't exist); `cookies={}` and in-loop calls are unaffected.
- **`set_cookie(domain=None)` now honors `path`.** The `domain=None` branch
returned right after `update_cookies()`, before the line that sets the
morsel's `path` — so a shared cookie (`set_cookie(name, value, path="/api")`,
no `domain=`) always stored the `SimpleCookie` default `path="/"` instead. The
morsel's `path` is now set before the `domain=None` early return. The
domain-bound branch was already correct and is unchanged.
### v0.1.10 ### v0.1.10
- Docs-only pass: compressed module/method docstrings and comments that restated - Docs-only pass: compressed module/method docstrings and comments that restated
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "aioweb" name = "aioweb"
version = "0.1.10" version = "0.1.11"
description = "Async HTTP session wrapper over aiohttp — proxies, header overwrites, retries, previews. Config-free, installable." description = "Async HTTP session wrapper over aiohttp — proxies, header overwrites, retries, previews. Config-free, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+8 -4
View File
@@ -227,12 +227,12 @@ class ExtendedSession:
""" """
cookie = SimpleCookie() cookie = SimpleCookie()
cookie[name] = value cookie[name] = value
cookie[name]["path"] = path
if domain is None: if domain is None:
self.session.cookie_jar.update_cookies(cookie) self.session.cookie_jar.update_cookies(cookie)
return return
if "://" not in domain: if "://" not in domain:
domain = "http://" + domain domain = "http://" + domain
cookie[name]["path"] = path
self.session.cookie_jar.update_cookies(cookie, response_url=URL(domain)) self.session.cookie_jar.update_cookies(cookie, response_url=URL(domain))
def clear_cookies(self): def clear_cookies(self):
@@ -247,11 +247,15 @@ class ExtendedSession:
# preview # preview
def preview(self, method, url, **kwargs): def preview(self, method, url, **kwargs):
"""build a RequestPreview for a request without sending it """build a RequestPreview for a request without sending it - synchronous, no loop required
url is domain-rewritten before cookies are resolved, matching the real url is domain-rewritten before cookies are resolved, matching the real
request - cookie binding is host-based, so resolving against the pre-rewrite request - cookie binding is host-based, so resolving against the pre-rewrite
host could miss or misattribute cookies. host could miss or misattribute cookies. the default cookie lookup never
touches the backend session: building it needs a running loop (aiohttp>=3.14),
which would defeat preview()'s own pre-loop inspection use case, and an unbuilt
session has no cookies to report anyway - pass cookies= explicitly to preview
cookies that would come from a not-yet-built session.
""" """
url = self._apply_domain_overwrites(url) url = self._apply_domain_overwrites(url)
proxy = self._get_proxy(url, kwargs.pop("proxies", None)) proxy = self._get_proxy(url, kwargs.pop("proxies", None))
@@ -262,7 +266,7 @@ class ExtendedSession:
timeout_total = timeout if isinstance(timeout, (int, float)) else None timeout_total = timeout if isinstance(timeout, (int, float)) else None
cookies = kwargs.pop("cookies", None) cookies = kwargs.pop("cookies", None)
if cookies is None: if cookies is None and self._session is not None:
cookies = self._cookies_for_url(url) cookies = self._cookies_for_url(url)
if cookies: if cookies:
cookie_header = "; ".join(f"{k}={v}" for k, v in cookies.items()) cookie_header = "; ".join(f"{k}={v}" for k, v in cookies.items())