rex-tls
rex-tls is a Python HTTP client backed by a native Rust TLS and HTTP core. It
provides a requests-style API with transport profiles for Android Chrome and
OkHttp, persistent sessions, cookies, proxies, streaming I/O, asynchronous
requests, and bounded connection pools.
Installation
python -m pip install rex-tls
Python 3.9 or newer is required. Supported platforms are:
- Windows x86-64
- Linux x86-64 with manylinux 2.28 or newer
Transport profiles
| Profile | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
chrome_android_149 |
Yes | Yes | Yes |
chrome_android_150 |
Yes | Yes | Yes |
okhttp_4.12 |
Yes | Yes | No |
okhttp_5.4 |
Yes | Yes | No |
chrome_android and chrome_android_latest are aliases for
chrome_android_150. okhttp and okhttp_latest are aliases for okhttp_5.4.
Use the versioned names when reproducibility matters.
Quick start
import rex_tls
response = rex_tls.get(
"https://example.com/",
profile="chrome_android_150",
params={"page": 1},
timeout=20,
)
response.raise_for_status()
print(response.status_code)
print(response.http_version)
print(response.text)
JSON and form requests use familiar keyword arguments:
response = rex_tls.post(
"https://api.example.com/items",
profile="okhttp_5.4",
json={"name": "example", "enabled": True},
)
response = rex_tls.post(
"https://api.example.com/form",
profile="okhttp_4.12",
data={"name": "example"},
)
Persistent sessions
A Session keeps cookies, connections, and TLS session state across requests.
from rex_tls import Session
with Session(
profile="chrome_android_150",
timeout=20,
) as session:
session.headers.update({"accept": "application/json"})
session.cookies.set("locale", "en-US")
first = session.get("https://example.com/api/profile")
second = session.get("https://example.com/api/settings")
print(second.connection_reused)
print(session.cookies.get_dict())
HTTP/1.1 concurrency can be bounded per origin and proxy route:
with Session(
profile="okhttp_5.4",
max_connections_per_route=4,
) as session:
...
The default is 1. HTTP/2 continues to multiplex streams on one connection;
additional connections are opened only after the route has negotiated HTTP/1.1
and every existing route connection is busy.
Per-request cookies are supported and do not modify the session jar:
response = session.get(
"https://example.com/api/items",
params={"page": 2},
cookies={"request-only": "value"},
headers={"accept": "application/json"},
)
Cookies
Session.cookies provides the commonly used requests-style CookieJar methods:
session.cookies.update({"theme": "dark"})
session.cookies.set(
"api-token",
"value",
domain="api.example.com",
path="/v1",
secure=True,
)
print(session.cookies.get("theme"))
print(session.cookies.get_dict())
print(session.cookies.items())
session.cookies.clear(domain="api.example.com", path="/v1", name="api-token")
Supported helpers include set(), set_cookie(), get(), get_dict(),
update(), clear(), keys(), values(), items(), list_domains(), and
list_paths(). Response cookies are validated against domain, path, Secure,
expiry, IP-address, and public-suffix rules before they enter the jar.
Proxies
Use proxy= as a single proxy for all supported routes:
proxy = "http://username:password@proxy.example:8080"
with Session("okhttp_5.4", proxy=proxy) as session:
response = session.get("https://example.com/")
Or use a requests-style mapping:
proxies = {
"http": "http://proxy.example:8080",
"https": "http://proxy.example:8080",
"all": "http://fallback.example:8080",
"no_proxy": ".internal.example,localhost,127.0.0.1",
}
with Session("chrome_android_149", proxies=proxies) as session:
response = session.get("https://example.com/")
Session.proxies is mutable. A per-request proxies= mapping overrides the
session mapping. Set trust_env=True to read HTTP_PROXY, HTTPS_PROXY,
ALL_PROXY, and NO_PROXY.
Proxy URLs must use http://. HTTPS destinations use HTTP CONNECT. Basic proxy
credentials are supported; percent-encode reserved characters in usernames and
passwords. HTTPS proxies, SOCKS, PAC, NTLM/Digest, and MASQUE are not supported.
HTTP version selection
The negotiated protocol is available as response.http_version.
Require an actual HTTP/2 connection with http2=True:
with Session("chrome_android_150", http2=True) as session:
response = session.get("https://example.com/")
assert response.http_version == "HTTP/2"
http2=True preserves the profile ALPN list and rejects the response if the
server does not negotiate HTTP/2. It supports HTTPS only and is mutually
exclusive with HTTP/3 modes.
Chrome profiles support the following http3 values:
| Value | Behavior |
|---|---|
off |
Disable HTTP/3. This is the default. |
auto |
Enable the Chrome HTTP/3 path with HTTP/2 or HTTP/1.1 fallback. |
only |
Require HTTP/3 and fail if it cannot be used. |
with Session("chrome_android_150", http3="auto") as session:
response = session.get("https://example.com/")
with Session("chrome_android_149", http3="only") as session:
response = session.get("https://example.com/")
assert response.http_version == "HTTP/3"
HTTP/3 requires HTTPS and a versioned Chrome profile. OkHttp profiles do not
enable HTTP/3. Conventional HTTP proxies cannot carry the QUIC path, so
http3="only" is rejected when such a proxy is selected.
Request headers
Headers may be supplied as a mapping or as ordered (name, value) pairs. The
native core applies the selected profile's protocol-specific ordering and casing
rules before transmission.
If both session and request headers are empty, the profile's default request headers are used. A non-empty caller header set is sent without adding unrelated profile defaults. Protocol-required fields and fields derived from cookies or the request body may still be generated.
HTTP/1.1 preserves the profile's observable field-name casing. HTTP/2 and HTTP/3
field names are lowercase as required by those protocols. Hop-by-hop HTTP/1.1
fields such as Connection are rejected for HTTP/2 and HTTP/3 instead of being
silently removed.
Streaming downloads
Use a context manager so the connection is released when the body reaches EOF or the response is closed:
from rex_tls import Session
with Session("chrome_android_150") as session:
with session.get("https://example.com/large.bin", stream=True) as response:
response.raise_for_status()
with open("large.bin", "wb") as output:
for chunk in response.iter_content(64 * 1024):
output.write(chunk)
iter_lines(), raw.read(), and raw.readinto() are also available. Set
decode_content=False to receive the compressed response body without automatic
content decoding.
Streaming uploads and multipart files
File objects and byte iterables are uploaded incrementally:
with Session("okhttp_5.4") as session:
with open("large.bin", "rb") as source:
response = session.post(
"https://example.com/upload",
content=source,
)
with open("image.png", "rb") as source:
response = session.post(
"https://example.com/form",
data={"title": "example"},
files={"file": ("image.png", source, "image/png")},
)
Seekable upload sources can be replayed across 307/308 redirects. A one-shot
source raises UnrewindableBodyError if a redirect requires replay.
Async API
AsyncSession provides an asyncio interface without blocking the event loop:
import asyncio
from rex_tls import AsyncSession
async def main() -> None:
async with AsyncSession(
"okhttp_5.4",
max_concurrency=8,
http2=True,
) as session:
urls = [f"https://example.com/items/{item}" for item in range(10)]
responses = await asyncio.gather(*(session.get(url) for url in urls))
print([response.status_code for response in responses])
asyncio.run(main())
Async streaming uses aiter_content() and aiter_lines():
from rex_tls import AsyncSession
async def stream_events(session: AsyncSession) -> None:
response = await session.get("https://example.com/events", stream=True)
async with response:
async for line in response.aiter_lines():
print(line)
Bounded session pools
SessionPool and AsyncSessionPool create multiple independent native sessions
with a fixed concurrency limit.
from concurrent.futures import ThreadPoolExecutor
from rex_tls import SessionPool
urls = [f"https://example.com/items/{item}" for item in range(20)]
with SessionPool(
"okhttp_4.12",
max_connections=8,
session_mode="shared",
) as pool:
pool.headers["accept"] = "application/json"
with ThreadPoolExecutor(max_workers=16) as executor:
responses = list(executor.map(pool.get, urls))
Pool session modes:
shared: members share headers, proxy configuration, and one thread-safe CookieJar. Connections remain independent.isolated: each member owns independent headers, proxy configuration, cookies, connections, and TLS state.
Lease a specific member when multiple requests must keep the same isolated state:
from rex_tls import SessionPool
with SessionPool(
"okhttp_5.4",
max_connections=4,
session_mode="isolated",
) as pool:
with pool.acquire(timeout=1) as account:
account.headers["authorization"] = "Bearer example-token"
account.cookies.set("account", "one")
account.get("https://example.com/step-1")
account.get("https://example.com/step-2")
Use pool_timeout= to limit how long a request waits for a pool member. The
ordinary timeout= argument continues to control the network request.
Response API
Common response attributes and methods include:
status_code,reason,url, andhttp_versionheaders, includingget_all()and the orderedrawviewcontent,text, andjson()okandraise_for_status()historyelapsed_secondslocal_addressandremote_addressconnection_reusedandtls_session_reusediter_content(),iter_lines(),close(), andaclose()
Requests compatibility
The API directly supports common requests-style arguments and attributes:
- HTTP method helpers and
Session params,headers,data,json,files, andcookiestimeout,verify,proxy, andproxiesSession.headers,Session.proxies,Session.cookies, andtrust_envstream=True,iter_content(),iter_lines(), and raw reads- per-request
allow_redirects .content,.text,.json(), and.raise_for_status()
It is not a drop-in replacement for every requests extension point. auth,
hooks, adapters, mount, and custom RequestsCookieJar policy objects are not
implemented.
TLS verification
Certificate verification is enabled by default and uses the installed certifi
CA bundle.
# Default CA bundle
Session("chrome_android_150", verify=True)
# Custom CA file
Session("chrome_android_150", verify="/path/to/private-ca.pem")
# Disable verification explicitly
Session("chrome_android_150", verify=False)
Disabling verification removes server identity protection and should only be used in controlled test environments.
Error handling
import rex_tls
try:
response = rex_tls.get(
"https://example.com/",
profile="chrome_android_150",
timeout=10,
)
response.raise_for_status()
except rex_tls.HTTPError as exc:
print(f"HTTP error: {exc}")
except rex_tls.RequestError as exc:
print(f"Request failed: {exc}")
except rex_tls.MobileTLSError as exc:
print(f"Native client error: {exc}")
except (TypeError, ValueError) as exc:
print(f"Invalid configuration: {exc}")
Additional exceptions include CookieConflictError, ContentDecodingError,
InvalidHeader, InvalidURL, SessionClosedError, StreamClosedError,
StreamConsumedError, and UnrewindableBodyError.
Local performance comparison
The following results were measured on Windows 11 with CPython 3.14 using
rex-tls 2.3.0, curl_cffi 0.15.0, never_primp 3.0.3, and httpcloak 1.6.5. Each
cell is median RPS / median P95 ms from three isolated process runs. Every run
used 128 measured requests, 32 warmups, a warm client, an empty request and
response body, and a localhost server with a fixed 10 ms response delay. P95
includes time waiting behind the selected concurrency bound.
HTTP/1.1 used a plain localhost server and each library's synchronous API, so
this table does not include TLS handshake or resumption time. rex-tls used
okhttp_4.12 with max_connections_per_route equal to concurrency;
never_primp used its Android OkHttp preset; curl_cffi used chrome; and
httpcloak used a bounded chrome-146 Session pool.
| Client | c=1 | c=8 | c=32 | c=128 |
|---|---|---|---|---|
| rex-tls | 93.5 / 11.0 | 731.9 / 171.6 | 2,610.6 / 47.3 | 3,753.6 / 19.6 |
| curl_cffi | 92.4 / 11.2 | 716.1 / 174.8 | 2,013.8 / 60.7 | 2,372.1 / 40.0 |
| never_primp | 94.1 / 10.9 | 740.0 / 170.3 | 2,720.1 / 45.6 | 4,553.3 / 17.3 |
| httpcloak | 93.5 / 11.0 | 702.4 / 180.7 | 2,261.2 / 55.4 | 3,695.8 / 32.5 |
HTTP/2 used a localhost TLS server with ALPN fixed to h2 and the native async
APIs of rex-tls, curl_cffi, and never_primp.
httpcloak's async Session was not stable under concurrent reuse in this runtime,
so its published row uses a bounded pool of synchronous HTTP/2 Sessions through
worker threads. rex-tls and never_primp reused one H2 connection in the measured
phase; curl_cffi and the httpcloak pool used multiple connections as concurrency
increased.
| Client | c=1 | c=8 | c=32 | c=128 |
|---|---|---|---|---|
| rex-tls | 64.5 / 1,893.4 | 298.8 / 412.7 | 1,095.2 / 114.5 | 1,851.6 / 66.6 |
| curl_cffi | 64.4 / 1,894.1 | 499.8 / 255.2 | 1,482.7 / 84.3 | 2,944.8 / 38.6 |
| never_primp | 63.4 / 1,890.6 | 495.4 / 257.8 | 1,785.7 / 70.9 | 2,236.8 / 56.0 |
| httpcloak | 64.8 / 1,882.0 | 473.9 / 254.6 | 1,071.7 / 116.2 | 1,084.8 / 111.8 |
The TLS-resumption check retained one synchronous Session while the TLS/H1
server closed every response connection. Each row is the median of three
isolated runs with 128 measured requests, eight warmups, concurrency 1, and the
same 10 ms server delay. Resumed is counted by the server from
SSLSocket.session_reused, not inferred from client timing.
| Client | RPS | P95 ms | Handshakes | Resumed |
|---|---|---|---|---|
| rex-tls | 85.6 | 12.9 | 128 | 128 |
| curl_cffi | 70.3 | 32.8 | 128 | 128 |
| never_primp | 72.3 | 30.2 | 128 | 0 |
| httpcloak | 82.9 | 13.0 | 128 | 0 |
The controlled H3 benchmark applies only to rex-tls because its server validates
the exact selected mobile profile instead of accepting a different client's
preset. It used one QUIC connection, 128 measured requests, at least 32 warmups,
1 ms response pacing, and five isolated runs. Cells are median RPS / median P95 ms; P95 remains queue-inclusive.
| Profile | c=1 | c=8 | c=32 | c=128 |
|---|---|---|---|---|
| chrome_android_149 | 91.6 / 1,331.6 | 666.4 / 182.1 | 660.1 / 182.6 | 678.3 / 180.7 |
| chrome_android_150 | 91.4 / 1,335.0 | 672.2 / 179.0 | 661.0 / 181.8 | 668.7 / 183.3 |
These loopback results compare client scheduling and transport overhead; they do
not establish universal network performance or fingerprint equivalence between
different presets. The summary JSON SHA-256 values are
21568fe7eac07157563167b77d5fa485ce7d165ded11d7754e9b1db721169b47
for H1 and
b3afb03b5845df52670a7dc2543e19b420f1f7e7fd2a3fd72a4a8a301e2653c8
for H2. Reproduce the matrix with tools/run_release_benchmarks.py; raw result
files and resource metrics remain outside the distribution. The H1 TLS
resumption summary SHA-256 is
a17faab8a594fc6765c17f19ddc1fe75015b39efada5d69d87cf6413118d469a.
The five-round H3 summary hashes are
ac9c08355cafa70be409b22c6f1e4422346c5ff372c5f4c19deed70b13f1795b
for Chrome Android 149 and
37d7b23156f2fb2912fa6421c29109e2b1ebe8519d9e0c59f155874c85220d3d
for Chrome Android 150.
Extended stress gates cap the H2 Pool at 32 physical connections. On Windows, long proxy-cold churn can return OS error 997 after hundreds of short-lived CONNECT/TLS sockets, so only its bounded functional smoke is a release gate; TLS ciphertext and request bodies are never automatically replayed. A regular HTTP CONNECT proxy also cannot carry QUIC, so H3 proxying would require a separate MASQUE/UDP-tunnel implementation and is not supported by this release.
Runtime information
import rex_tls
print(rex_tls.__version__)
print(rex_tls.profiles())
print(dict(rex_tls.native_versions()))
print(rex_tls.profile_info("chrome_android_150"))
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rex_tls-2.3.0-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: rex_tls-2.3.0-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 2.4 MB
- Tags: CPython 3.9+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
269195695f02bfee7ab75e971ce8f11a45478bb9613838ed7f97e505b8680347
|
|
| MD5 |
3481cbe2fde08ad75df9b2b0431f2c5b
|
|
| BLAKE2b-256 |
55eb28e35e4efa2d1191cc1c24ed96b71b549b1bb2e94bc3907ae6653c6e3f52
|
File details
Details for the file rex_tls-2.3.0-cp39-abi3-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: rex_tls-2.3.0-cp39-abi3-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.9+, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e8896553341428b7d6edda8e2217c0eee3a23a8dbe895171f020c4643f3b212
|
|
| MD5 |
65b03dbfcac263d20516594ff4526e22
|
|
| BLAKE2b-256 |
e3a20f51a3a0c2d6824636d9c707dd7be9049180fb972f02eb9077a9e9e40dfc
|