fix: wrap backend-native exceptions; correct setup/desc docs (v0.1.3)

- CurlCffi/Noble raw_request translate backend-native network errors (curl_cffi
  RequestException, noble_tls TLSClientException) into aiohttp.ClientError so the bare
  request() path gives the same typed-failure contract as the aiohttp backend (L6)
- Noble.setup uses download_if_necessary (the current noble_tls API), with
  update_if_necessary only as a fallback; docstring/CLAUDE.md no longer claim the dead
  'refreshes an existing one' path (L7)
- pyproject description says composition (one injectable TLSSession), not the old
  'ExtendedSession subclasses' (L8).

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
disqualifier 2026-06-29 17:57:54 -04:00
parent 5eb689ba73
commit ccea880df0
3 changed files with 38 additions and 17 deletions

View File

@ -22,17 +22,17 @@ you want; importing the package never fails because an extra is missing.
`requirements.txt` (pick the extra you need): `requirements.txt` (pick the extra you need):
``` ```
aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2 aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2 aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2 aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3
``` ```
Direct: Direct:
```bash ```bash
pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2" pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2" pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.2" pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.3"
``` ```
- `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both. - `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both.

View File

@ -4,8 +4,8 @@ build-backend = "hatchling.build"
[project] [project]
name = "aioweb_tls" name = "aioweb_tls"
version = "0.1.2" version = "0.1.3"
description = "TLS-fingerprinting backends for aioweb — curl_cffi / noble_tls ExtendedSession subclasses, config-free, installable." description = "TLS-fingerprinting backends (curl_cffi / noble_tls) for aioweb via one injectable TLSSession, config-free, installable."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.0", "aioweb @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb.git@v0.1.0",

View File

@ -15,10 +15,23 @@ import asyncio
import logging import logging
import math import math
import aiohttp
from aioweb import Response from aioweb import Response
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _as_client_error(error: Exception, backend: str) -> aiohttp.ClientError:
"""wrap a backend-native network exception as an aiohttp.ClientError
aioweb's request() only re-wraps aiohttp.ClientError; curl_cffi raises
RequestException(OSError) and noble_tls raises TLSClientException(IOError), neither
of which is an aiohttp.ClientError. translating here gives TLS backends the same
typed failure contract as the aiohttp path on the bare request() route.
"""
return aiohttp.ClientError(f"{backend} request failed: {error}")
try: try:
from curl_cffi import AsyncSession as _CurlAsyncSession from curl_cffi import AsyncSession as _CurlAsyncSession
_CURL_ERROR = None _CURL_ERROR = None
@ -94,10 +107,13 @@ class CurlCffi:
if proxy: if proxy:
kwargs["proxy"] = proxy kwargs["proxy"] = proxy
try:
response = await session.request(method, url, impersonate=impersonate, **kwargs) response = await session.request(method, url, impersonate=impersonate, **kwargs)
content = response.content except aiohttp.ClientError:
if content is None: raise
content = response.text.encode() if response.text else b"" except Exception as error:
raise _as_client_error(error, "curl_cffi") from error
content = response.content if response.content is not None else b""
return Response( return Response(
status_code=response.status_code, status_code=response.status_code,
headers=dict(response.headers), headers=dict(response.headers),
@ -161,9 +177,9 @@ class Noble:
async def setup(self) -> None: async def setup(self) -> None:
"""fetch the noble_tls Go shared library once; idempotent and concurrency-safe """fetch the noble_tls Go shared library once; idempotent and concurrency-safe
download_if_necessary handles the first-time fetch (no lib present); uses noble_tls.download_if_necessary (the current API: it fetches the asset on
update_if_necessary refreshes an existing one. try download first so a first use and no-ops when it already exists). older noble_tls without that name
clean environment works, falling back to update. is handled via update_if_necessary as a fallback.
guarded by an asyncio.Lock with a check-lock-recheck so concurrent first guarded by an asyncio.Lock with a check-lock-recheck so concurrent first
requests don't both run the fetch: the fast path returns once _updated is requests don't both run the fetch: the fast path returns once _updated is
@ -177,7 +193,7 @@ class Noble:
download = getattr(noble_tls, "download_if_necessary", None) download = getattr(noble_tls, "download_if_necessary", None)
if download is not None: if download is not None:
await download() await download()
else: elif hasattr(noble_tls, "update_if_necessary"):
await noble_tls.update_if_necessary() await noble_tls.update_if_necessary()
self._updated = True self._updated = True
@ -210,7 +226,12 @@ class Noble:
if proxy: if proxy:
kwargs["proxy"] = proxy kwargs["proxy"] = proxy
try:
response = await session.execute_request(method=method.upper(), url=url, **kwargs) response = await session.execute_request(method=method.upper(), url=url, **kwargs)
except aiohttp.ClientError:
raise
except Exception as error:
raise _as_client_error(error, "noble_tls") from error
content = getattr(response, "content", None) content = getattr(response, "content", None)
if content is None: if content is None:
text = getattr(response, "text", "") or "" text = getattr(response, "text", "") or ""