Skip to main content

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, and http_version
  • headers, including get_all() and the ordered raw view
  • content, text, and json()
  • ok and raise_for_status()
  • history
  • elapsed_seconds
  • local_address and remote_address
  • connection_reused and tls_session_reused
  • iter_content(), iter_lines(), close(), and aclose()

Requests compatibility

The API directly supports common requests-style arguments and attributes:

  • HTTP method helpers and Session
  • params, headers, data, json, files, and cookies
  • timeout, verify, proxy, and proxies
  • Session.headers, Session.proxies, Session.cookies, and trust_env
  • stream=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

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rex_tls-2.3.0-cp39-abi3-win_amd64.whl (2.4 MB view details)

Uploaded CPython 3.9+Windows x86-64

rex_tls-2.3.0-cp39-abi3-manylinux_2_28_x86_64.whl (2.6 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ x86-64

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

Hashes for rex_tls-2.3.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 269195695f02bfee7ab75e971ce8f11a45478bb9613838ed7f97e505b8680347
MD5 3481cbe2fde08ad75df9b2b0431f2c5b
BLAKE2b-256 55eb28e35e4efa2d1191cc1c24ed96b71b549b1bb2e94bc3907ae6653c6e3f52

See more details on using hashes here.

File details

Details for the file rex_tls-2.3.0-cp39-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for rex_tls-2.3.0-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 6e8896553341428b7d6edda8e2217c0eee3a23a8dbe895171f020c4643f3b212
MD5 65b03dbfcac263d20516594ff4526e22
BLAKE2b-256 e3a20f51a3a0c2d6824636d9c707dd7be9049180fb972f02eb9077a9e9e40dfc

See more details on using hashes here.

Release history Release notifications | RSS feed

2.24.1

2 files

2.24.0

2 files

2.23.0

2 files

2.22.0

2 files

2.21.0

2 files

2.20.2

2 files

2.20.1

2 files

2.20.0

2 files

2.19.2

2 files

2.19.1

2 files

2.19.0

4 files

2.18.1

4 files

2.18.0

2 files

2.17.0

2 files

2.16.3

2 files

2.16.2

2 files

2.16.1

2 files

2.16.0

2 files

2.15.2

2 files

2.15.1

2 files

2.15.0

2 files

2.14.0

2 files

2.13.0

2 files

2.12.0

2 files

2.11.0

2 files

2.10.1

2 files

2.10.0

2 files

2.9.0

2 files

2.8.0

2 files

2.7.0

2 files

2.6.0

2 files

2.5.0

2 files

2.4.0

2 files

2.3.2

2 files

2.3.1

2 files

This release

2.3.0 This release

2 files

2.2.1

2 files

2.2.0

2 files

2.1.0

2 files

2.0.0

2 files

0.1.1

5 files

0.1.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page