fix: label TLS-backend timeouts ServerTimeoutError, flatten noble multi headers, ValueError on unknown profile (v0.1.5)

Both backends' asyncio.TimeoutError except-branch was dead code (neither curl_cffi
nor noble_tls raises it), so a real wire timeout fell through to the generic OSError
branch and got mislabeled a plain aiohttp.ClientError instead of aioweb's own
ServerTimeoutError contract; now detected via curl_cffi's Timeout type or Go-side
timeout text and re-wrapped correctly. Noble's multi-valued response headers (e.g.
two Set-Cookie lines) arrived as Python lists instead of strings, breaking any
downstream .split()/.lower() call; now comma-joined per RFC 7230. An unknown Noble
client profile string raised a raw AttributeError from the enum lookup; now a
ValueError listing the valid profile names. Also compresses essay-length docstrings
and narrating comments across the module with no behavior change.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-02 23:28:42 -04:00
parent eb7745ae9b
commit 330aec4260
5 changed files with 143 additions and 96 deletions
+17 -8
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.4 aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5
aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.4 aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.4 aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5
``` ```
Direct: Direct:
```bash ```bash
pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.4" pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5"
pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.4" pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5"
pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.4" pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.5"
``` ```
- `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both. - `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both.
@@ -44,7 +44,7 @@ pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-publi
Constructing a backend whose client isn't installed raises that `RuntimeError` at Constructing a backend whose client isn't installed raises that `RuntimeError` at
construction, never at import. construction, never at import.
Drop the `@v0.1.4` suffix from the line above to install the latest unpinned. Drop the `@v0.1.5` suffix from the line above to install the latest unpinned.
## curl_cffi backend ## curl_cffi backend
@@ -63,6 +63,10 @@ async with TLSSession(backend=CurlCffi(impersonate="chrome"), proxies={"https":
backend kwargs — passing `impersonate=` there raises `TypeError`; set the profile on backend kwargs — passing `impersonate=` there raises `TypeError`; set the profile on
the `CurlCffi` instance for the retrying path. the `CurlCffi` instance for the retrying path.
- curl_cffi forges JA3/JA4 + HTTP/2 fingerprints via the bundled curl-impersonate binary. - curl_cffi forges JA3/JA4 + HTTP/2 fingerprints via the bundled curl-impersonate binary.
- A wire-level timeout raises `aiohttp.ServerTimeoutError` (matching aioweb's own
contract), not a generic `aiohttp.ClientError` — both backends detect their
native timeout (curl_cffi's `Timeout` type, noble_tls's Go-side timeout text) and
re-wrap it before the fallback client-error path (v0.1.5).
## noble backend ## noble backend
@@ -76,7 +80,8 @@ async with TLSSession(backend=Noble(client="chrome_133")) as s:
print(resp.json()["tls"]["ja3"]) print(resp.json()["tls"]["ja3"])
``` ```
- `Noble(client="chrome_133")` — accepts a `noble_tls.Client` enum or a string name. - `Noble(client="chrome_133")` — accepts a `noble_tls.Client` enum or a string name;
an unknown string raises `ValueError` listing the valid profile names (v0.1.5).
- noble_tls downloads a Go shared library on first use. `await s.setup()` fetches it - noble_tls downloads a Go shared library on first use. `await s.setup()` fetches it
once at startup; if you skip it, the first request fetches it lazily. The fetch is once at startup; if you skip it, the first request fetches it lazily. The fetch is
guarded by a lock, so even concurrent first requests download it exactly once. guarded by a lock, so even concurrent first requests download it exactly once.
@@ -85,6 +90,10 @@ async with TLSSession(backend=Noble(client="chrome_133")) as s:
bytes (`U+FFFD` replacement, wrong length) even on a 200 response — the Noble bytes (`U+FFFD` replacement, wrong length) even on a 200 response — the Noble
backend always requests `is_byte_response=True` and decodes the resulting backend always requests `is_byte_response=True` and decodes the resulting
base64 data-URI back into raw bytes, so `resp.content` is never lossy. base64 data-URI back into raw bytes, so `resp.content` is never lossy.
- Multi-valued response headers (e.g. two `Set-Cookie` lines) arrive from noble_tls's
Go side as a Python list — the Noble backend flattens them to a single
comma-joined string per RFC 7230, so `resp.headers[...]` is always a plain string
(v0.1.5).
## Writing your own backend (the `TLSBackend` protocol) ## Writing your own backend (the `TLSBackend` protocol)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "aioweb_tls" name = "aioweb_tls"
version = "0.1.4" version = "0.1.5"
description = "TLS-fingerprinting backends (curl_cffi / noble_tls) for aioweb via one injectable TLSSession, 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 = [
+107 -66
View File
@@ -25,14 +25,40 @@ log = logging.getLogger(__name__)
def _as_client_error(error: Exception, backend: str) -> aiohttp.ClientError: def _as_client_error(error: Exception, backend: str) -> aiohttp.ClientError:
"""wrap a backend-native network exception as an aiohttp.ClientError """wrap a backend-native network exception as an aiohttp.ClientError
aioweb's request() only re-wraps aiohttp.ClientError; curl_cffi raises curl_cffi raises RequestException(OSError), noble_tls raises
RequestException(OSError) and noble_tls raises TLSClientException(IOError), neither TLSClientException(IOError) — neither is an aiohttp.ClientError, which is all
of which is an aiohttp.ClientError. translating here gives TLS backends the same aioweb's request() re-wraps; translating here gives both the same typed contract.
typed failure contract as the aiohttp path on the bare request() route.
""" """
return aiohttp.ClientError(f"{backend} request failed: {error}") return aiohttp.ClientError(f"{backend} request failed: {error}")
_TIMEOUT_TEXT = ("timeout", "timed out", "deadline exceeded")
def _is_timeout_error(error: Exception) -> bool:
"""whether a backend-native network error is a timeout
neither backend raises asyncio.TimeoutError for a wire timeout: curl_cffi's own
Timeout and noble_tls's Go-side TLSClientException are both plain OSError
subclasses with no dedicated timeout type — check curl_cffi's Timeout class by
name first (precise), then fall back to matching the error text (covers
noble_tls's Go messages, e.g. "context deadline exceeded").
"""
timeout_type = getattr(error, "__class__", None)
if timeout_type is not None and any(base.__name__ == "Timeout" for base in timeout_type.__mro__):
return True
return any(needle in str(error).lower() for needle in _TIMEOUT_TEXT)
def _as_timeout_error(error: Exception, backend: str) -> aiohttp.ServerTimeoutError:
"""wrap a backend-native timeout as aiohttp.ServerTimeoutError
matches aioweb's own contract (a total timeout raises ServerTimeoutError, both a
ClientError and a TimeoutError) instead of surfacing as a generic client error.
"""
return aiohttp.ServerTimeoutError(f"{backend} request timed out: {error}")
try: try:
from curl_cffi import AsyncSession as _CurlAsyncSession from curl_cffi import AsyncSession as _CurlAsyncSession
_CURL_ERROR = None _CURL_ERROR = None
@@ -51,10 +77,10 @@ except ImportError as error:
def _coerce_timeout(value): def _coerce_timeout(value):
"""turn aioweb's aiohttp ClientTimeout (or a number) into a plain number """unwrap aioweb's aiohttp ClientTimeout (or a plain number) to a number
aioweb.request() wraps a numeric timeout in an aiohttp.ClientTimeout before the aioweb.request() wraps a numeric timeout in aiohttp.ClientTimeout before the seam
seam sees it; the tls clients want a number, so unwrap .total when present. sees it; the tls clients want a bare number.
""" """
total = getattr(value, "total", value) total = getattr(value, "total", value)
return total if isinstance(total, (int, float)) else None return total if isinstance(total, (int, float)) else None
@@ -63,12 +89,10 @@ def _coerce_timeout(value):
def _noble_timeout_seconds(value): def _noble_timeout_seconds(value):
"""coerce a raw or wrapped timeout into whole seconds for noble's Go int field """coerce a raw or wrapped timeout into whole seconds for noble's Go int field
single source of truth for both of Noble's timeout paths (create_session's single source of truth for both Noble timeout paths (session-default and
session-default and raw_request's per-call override): noble's Go field per-call): the Go field timeoutSeconds is an int, so a sub-second float truncates
timeoutSeconds is an int, so a sub-second float (e.g. 0.5) truncates to 0 — which to 0 (= no timeout) and a non-integer fails Go-side JSON unmarshal outright.
Go reads as no/instant timeout — and any non-integer value fails Go-side JSON rounds up so a sub-second timeout still waits at least 1s. None if uncoercible.
unmarshal outright. rounds UP so a sub-second timeout still waits at least 1s
instead of truncating away. returns None when there is no timeout to coerce.
""" """
timeout = _coerce_timeout(value) timeout = _coerce_timeout(value)
if timeout is None: if timeout is None:
@@ -80,11 +104,10 @@ def _noble_content(response) -> bytes:
"""extract true response bytes from a noble_tls response fetched with is_byte_response """extract true response bytes from a noble_tls response fetched with is_byte_response
with is_byte_response=True, noble_tls's Go side returns the body as a with is_byte_response=True, noble_tls's Go side returns the body as a
`data:<mime>;base64,<payload>` URI string in response.text (the whole Go response is `data:<mime>;base64,<payload>` URI string in response.text (the whole Go response
UTF-8-decoded before JSON parsing, so raw bytes have to travel as base64 to survive is UTF-8-decoded before JSON parsing, so raw bytes travel as base64) —
that round trip) — response.content is useless here since it is just response.content just re-encodes that string, so decode the data-URI here
`self.text.encode()`, re-encoding the data-URI string itself rather than decoding it. instead. falls back to a plain utf-8 encode if the body isn't a data-URI.
falls back to a plain utf-8 encode if the body isn't a data-URI (e.g. an error body).
""" """
text = getattr(response, "text", "") or "" text = getattr(response, "text", "") or ""
if text.startswith("data:") and ";base64," in text: if text.startswith("data:") and ";base64," in text:
@@ -93,12 +116,25 @@ def _noble_content(response) -> bytes:
return text.encode() return text.encode()
def _flatten_headers(headers) -> dict:
"""flatten a Go-style map[str][]str header dict into plain str values
noble_tls keeps a multi-valued header (e.g. two Set-Cookie lines) as a Python
list, breaking any downstream .split()/.lower() call; join with ", " per RFC 7230
field-value combination, leaving single values untouched.
"""
return {
key: ", ".join(value) if isinstance(value, list) else value
for key, value in headers.items()
}
def _jar_to_dict(session): def _jar_to_dict(session):
"""best-effort map of a requests-style cookie jar on session to a plain dict """best-effort map of a requests-style cookie jar on session to a plain dict
intentionally broad: this feeds preview() only, the two backends expose differently- feeds preview() only; the two backends expose differently-shaped jars and a
shaped jars, and a cookie read must never crash a request so any jar that doesn't cookie read must never crash a request, so a jar that fails to iterate degrades
iterate cleanly degrades to {} rather than raising. to {} rather than raising.
""" """
jar = getattr(session, "cookies", None) jar = getattr(session, "cookies", None)
if not jar: if not jar:
@@ -114,10 +150,10 @@ class CurlCffi:
config: config:
impersonate: browser profile to forge (default "chrome"); override per call impersonate: browser profile to forge (default "chrome"); override per call
by passing impersonate= to the low-level request()/_raw_request path, by passing impersonate= to the low-level request()/_raw_request path
which forwards **kwargs to the backend. NOT request_with_retries — its (forwards **kwargs to the backend) — NOT request_with_retries, whose
signature is fixed (no **kwargs) and would raise TypeError. for a fixed signature has no **kwargs and raises TypeError. for the retrying
per-call profile under retries, set it on the CurlCffi instance instead. path, set the profile on the CurlCffi instance instead.
requires the [curl] extra (pip install "aioweb_tls[curl]"). requires the [curl] extra (pip install "aioweb_tls[curl]").
""" """
@@ -133,13 +169,11 @@ class CurlCffi:
def create_session(self, headers, timeout, **kwargs): def create_session(self, headers, timeout, **kwargs):
"""build the curl_cffi AsyncSession """build the curl_cffi AsyncSession
deliberately does NOT pass `headers` to AsyncSession: curl_cffi bakes a deliberately does NOT pass `headers`: curl_cffi bakes a constructor
constructor `headers=` into the client and re-merges it under whatever `headers=` into the client and re-merges it under per-request headers,
per-request headers() sends, so update_headers()/clear_headers() would stop which would desync update_headers()/clear_headers() from what's actually on
matching what's actually on the wire (aioweb's session-default headers are the wire aioweb's session-default headers already apply per request via
already applied per request by the base's _default_headers merge — see the base's _default_headers merge (see ExtendedSession._create_session).
aioweb.ExtendedSession._create_session's docstring for why baking breaks the
mutable header api).
""" """
return _CurlAsyncSession(timeout=timeout, **kwargs) return _CurlAsyncSession(timeout=timeout, **kwargs)
@@ -162,9 +196,11 @@ class CurlCffi:
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise raise
except OSError as error: except OSError as error:
# curl_cffi's RequestException subclasses OSError; translate the native # RequestException (incl. curl_cffi's own Timeout) subclasses OSError, never
# network error into aiohttp.ClientError. narrowed from a bare Exception so a # asyncio.TimeoutError — check for a timeout first so it surfaces as
# real bug (AttributeError/TypeError) isn't laundered into 'client error' # ServerTimeoutError; narrowed from bare Exception so a real bug surfaces
if _is_timeout_error(error):
raise _as_timeout_error(error, "curl_cffi") from error
raise _as_client_error(error, "curl_cffi") from error raise _as_client_error(error, "curl_cffi") from error
content = response.content if response.content is not None else b"" content = response.content if response.content is not None else b""
return Response( return Response(
@@ -179,10 +215,9 @@ class CurlCffi:
def is_closed(self, session) -> bool: def is_closed(self, session) -> bool:
"""whether the curl_cffi session is closed """whether the curl_cffi session is closed
curl_cffi tracks closed state in the private `_closed` (no public `closed` curl_cffi tracks closed state in the private `_closed` (no public `closed`);
property), so read that; fall back to a public `closed` if a future version fall back to a public `closed` if a future version adds one. TLSSession's own
adds one. TLSSession's own `_closed` flag is the primary signal — this is a `_closed` flag is the primary signal — this is a best-effort out-of-band check.
best-effort backend check for out-of-band closes.
""" """
closed = getattr(session, "_closed", None) closed = getattr(session, "_closed", None)
if closed is None: if closed is None:
@@ -234,21 +269,29 @@ class Noble:
@staticmethod @staticmethod
def _resolve_client(client): def _resolve_client(client):
"""turn a string or Client enum into a noble_tls Client value""" """turn a string or Client enum into a noble_tls Client value
if isinstance(client, str):
return getattr(_NobleClient, client.upper()) raises ValueError naming the valid profiles for an unknown string, instead of
return client letting getattr's raw AttributeError leak the enum's internal lookup mechanics.
"""
if not isinstance(client, str):
return client
name = client.upper()
resolved = getattr(_NobleClient, name, None)
if resolved is None:
valid = ", ".join(member.name for member in _NobleClient)
raise ValueError(f"unknown noble_tls client profile {client!r}; valid profiles: {valid}")
return resolved
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
uses noble_tls.download_if_necessary (the current API: it fetches the asset on uses noble_tls.download_if_necessary (fetches on first use, no-ops if
first use and no-ops when it already exists). older noble_tls without that name present); falls back to update_if_necessary on older noble_tls.
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 — only the first caller through the lock
set, and only the first caller through the lock does the work. does the work, the rest see _updated already set.
""" """
if self._updated: if self._updated:
return return
@@ -266,18 +309,14 @@ class Noble:
"""build the noble_tls Session, honoring the session-default timeout """build the noble_tls Session, honoring the session-default timeout
noble_tls.Session takes neither headers nor timeout in its constructor. noble_tls.Session takes neither headers nor timeout in its constructor.
deliberately does NOT bake `headers` into session.headers: aioweb's deliberately does NOT bake `headers` into session.headers, same rationale as
session-default headers are already applied per request by the base's CurlCffi.create_session — aioweb's per-request _default_headers merge already
_default_headers merge, and baking them here would make applies them, so baking here would desync update_headers()/clear_headers().
update_headers()/clear_headers() stop matching what's actually on the wire
(see aioweb.ExtendedSession._create_session's docstring, and CurlCffi.create_session
above for the same rationale) — the per-request merge means headers are never
silently dropped, contrary to what this docstring used to claim.
the coerced timeout IS applied here (matching raw_request's per-call path): noble's the coerced timeout IS applied here (matching raw_request's per-call path):
Go field timeoutSeconds is an int, so a raw sub-second/float session-default (e.g. noble's Go field timeoutSeconds is an int, so an uncoerced sub-second/float
timeout=7.5) would fail Go-side JSON unmarshal on every request that doesn't session-default would fail Go-side JSON unmarshal on any request that doesn't
override it per call. max(1, ceil()) mirrors the guard raw_request already has. override it per call; max(1, ceil()) mirrors raw_request's own guard.
""" """
session = noble_tls.Session(client=self.client, **kwargs) session = noble_tls.Session(client=self.client, **kwargs)
timeout_seconds = _noble_timeout_seconds(timeout) timeout_seconds = _noble_timeout_seconds(timeout)
@@ -297,10 +336,9 @@ class Noble:
if proxy: if proxy:
kwargs["proxy"] = proxy kwargs["proxy"] = proxy
# force byte-safe transport: without this, noble_tls's Go side returns the body # byte-safe transport: without this, noble_tls's Go side returns the body as a
# as a plain UTF-8 JSON string, replacing invalid bytes with U+FFFD — silently # plain UTF-8 JSON string (U+FFFD-mangling any binary payload on a 200 response);
# corrupting any binary payload (image/zip/pdf) even though the request succeeds # see _noble_content() for the base64 data-URI decode this pairs with.
# with status 200. see _noble_content() for how the byte-safe body is decoded back.
kwargs.setdefault("is_byte_response", True) kwargs.setdefault("is_byte_response", True)
try: try:
@@ -310,13 +348,16 @@ class Noble:
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise raise
except OSError as error: except OSError as error:
# noble_tls's TLSClientException subclasses IOError (== OSError); translate # TLSClientException subclasses IOError (== OSError); the Go side has no
# the native network error, narrowed from bare Exception so a real bug surfaces # distinct timeout type either, just a text body ("context deadline
# exceeded") — check for one before the generic client-error wrap
if _is_timeout_error(error):
raise _as_timeout_error(error, "noble_tls") from error
raise _as_client_error(error, "noble_tls") from error raise _as_client_error(error, "noble_tls") from error
content = _noble_content(response) content = _noble_content(response)
return Response( return Response(
status_code=response.status_code, status_code=response.status_code,
headers=dict(getattr(response, "headers", {}) or {}), headers=_flatten_headers(getattr(response, "headers", {}) or {}),
content=content, content=content,
url=str(getattr(response, "url", url)), url=str(getattr(response, "url", url)),
reason=getattr(response, "reason", None), reason=getattr(response, "reason", None),
+16 -19
View File
@@ -6,42 +6,39 @@ to one HTTP client. TLSSession owns the live session object (built by create_ses
and passes it into every backend call, so backends hold no per-request state. and passes it into every backend call, so backends hold no per-request state.
implement this protocol to add a custom backend (e.g. a local Go TLS server); inject implement this protocol to add a custom backend (e.g. a local Go TLS server); inject
it via TLSSession(backend=MyBackend(...)) and it inherits all of aioweb's domain / it via TLSSession(backend=MyBackend(...)) and it inherits aioweb's domain / header /
header / ephemeral / proxy / retry / preview logic unchanged — those operate on plain ephemeral / proxy / retry / preview logic unchanged — those operate on plain dicts
dicts and never touch the backend. and never touch the backend.
required: required:
create_session(headers: dict, timeout, **kwargs) -> session create_session(headers: dict, timeout, **kwargs) -> session
build and return the live client session object. TLSSession stores it as build and return the live client session object, stored as self.session and
self.session and hands it back to every other method below. passed to every method below.
async raw_request(session, method, url, **kwargs) -> aioweb.Response async raw_request(session, method, url, **kwargs) -> aioweb.Response
send one request with `session` and adapt the client's response into an send one request; adapt the client's response into an aioweb.Response built
aioweb.Response built from primitives (status_code, headers, content bytes, from primitives (status_code, headers, content bytes, url, reason). kwargs
url, reason). kwargs arrive aioweb-shaped — the base has already resolved the arrive aioweb-shaped: proxy resolved into kwargs["proxy"], headers merged
proxy into kwargs["proxy"] and merged headers into kwargs["headers"], and a into kwargs["headers"], numeric timeout wrapped in aiohttp.ClientTimeout
numeric timeout is wrapped in an aiohttp.ClientTimeout (unwrap .total). (unwrap .total).
is_closed(session) -> bool is_closed(session) -> bool
whether `session` is closed. whether `session` is closed.
optional: optional:
cookies_for_url(session, url) -> dict cookies_for_url(session, url) -> dict
cookies the client would send for url, for preview(). default {} (used when cookies the client would send for url, for preview(). default {}.
the backend has no introspectable jar).
set_cookie(session, name, value, domain=None, path="/") -> None set_cookie(session, name, value, domain=None, path="/") -> None
get_cookies(session) -> dict get_cookies(session) -> dict
clear_cookies(session) -> None clear_cookies(session) -> None
the mutable cookie api TLSSession.set_cookie/get_cookies/clear_cookies back TLSSession's mutable cookie api. the aiohttp-only base reaches into
delegate to. the aiohttp-only base implementations reach into session.cookie_jar, which TLS backends lack, so an implementation without
session.cookie_jar, which TLS backends don't have, so a backend without these raises NotImplementedError instead of a private-attribute AttributeError.
these raises NotImplementedError from TLSSession rather than crashing with
an AttributeError on a private aiohttp attribute.
async setup() -> None async setup() -> None
one-time async preparation (e.g. fetch a native lib). called once via one-time async preparation (e.g. fetch a native lib); idempotent. called via
TLSSession.setup() and lazily before the first request; make it idempotent. TLSSession.setup() and lazily before the first request.
async close(session) -> None async close(session) -> None
close `session`. default awaits session.close() if present. close `session`. default awaits session.close() if present.
+2 -2
View File
@@ -4,8 +4,8 @@ TLSSession — one aioweb session, any tls backend
TLSSession subclasses aioweb.ExtendedSession and delegates only the four backend TLSSession subclasses aioweb.ExtendedSession and delegates only the four backend
seams to an injected backend object (see protocol.py). everything else — header seams to an injected backend object (see protocol.py). everything else — header
overwrites, domain rewriting, ephemeral headers, proxies, retries, previews — is overwrites, domain rewriting, ephemeral headers, proxies, retries, previews — is
inherited from aioweb unchanged, because that logic operates on plain dicts and inherited unchanged, since that logic operates on plain dicts and never touches
never touches the backend. the backend.
from aioweb_tls import TLSSession, CurlCffi, Noble from aioweb_tls import TLSSession, CurlCffi, Noble