rex-tls
rex-tls is a Python HTTP client with a native Rust TLS and HTTP core. It offers a familiar Requests-style API and transport profiles for Android Chrome, WebView, Cronet, OkHttp, Ktor, HttpsURLConnection/Conscrypt, and Flutter dart:io. It includes persistent connections, cookies, HTTP and SOCKS5 proxies, streaming I/O, asyncio support, and bounded session pools.
The native core owns TLS handshakes, ALPN, HTTP/1.1 serialization, HTTP/2 framing, HTTP/3 over QUIC, request-header ordering, response framing, and connection reuse. Python code provides the public API and Requests-compatible objects.
Highlights
- Versioned Android Chrome 149 and 150 profiles with HTTP/1.1, HTTP/2, and HTTP/3 support.
- A versioned Android Cronet 151 profile with HTTP/1.1, HTTP/2, and HTTP/3 support.
- Versioned Android WebView 140 through 150 profiles with HTTP/1.1 and HTTP/2 support.
- Versioned OkHttp 4.12 and 5.4 profiles with HTTP/1.1 and HTTP/2 support.
- Three Android Ktor 3.5.2 profiles covering the CIO, Android URLConnection, and OkHttp 5.3.2 engines.
- A platform Android 11 HttpsURLConnection profile and four hash-locked, application-bundled Conscrypt profiles with verified HTTP/1.1 behavior.
- A versioned Flutter 3.47.1 / Dart 3.13.1 Android profile for the default
HTTP/1.1-only
dart:io HttpClienttransport and Dio IO adapter. - Five versioned Flutter iOS profiles from 3.35.7 through 3.47.1, each built, signed, captured, and validated on a physical iOS device.
- Requests-style Session.request(), method helpers, CookieJar operations, redirects, timeouts, proxies, streaming, and Response properties.
- Native connection reuse and HTTP/2 or HTTP/3 multiplexing.
- Synchronous, asynchronous, shared-state pool, and isolated-state pool APIs.
- Incremental uploads and downloads with bounded memory use.
What's new in 2.17.0
- Replaced one-worker-per-request orchestration for eligible strict HTTP/2
AsyncSessioncalls with one bounded completion dispatcher. Existingrequest(),get(), and other method helpers are unchanged. - Reduced the measured Python thread peak at HTTP/2 concurrency 128 from about 131 to 4 while retaining one physical HTTP/2 connection. Controlled Windows and Linux comparisons met the release throughput and p95 latency gates at concurrency 1, 8, 32, and 128.
- Preserved Session CookieJar reads and writes, response cookies, transparent
content decoding, request metadata, cancellation, total timeouts, and
Requests-compatible
response.nexton the dispatcher path. - Made completion failures request-scoped: a corrupt compressed response or other response-finalization error no longer fails unrelated multiplexed requests. Dispatcher queue time is included in the request timeout.
- Requests that need streaming, a body upload, redirect following, an explicit request Cookie mapping, a proxy override, query-parameter merging, navigation context, or a split timeout continue through the established compatibility path automatically.
- Replaced short-lived Windows TCP wake pairs with private UDP loopback wake sockets for H2 and H3 actors, eliminating repeated-creation Winsock resource exhaustion without changing remote traffic.
- No built-in ClientHello, TLS record, HTTP/1.1 request, HTTP/2 frame, HPACK, HTTP/3 packet, QPACK, header-order, or profile wire configuration changed.
What's new in 2.16.3
- Added a Linux H2 actor wake path using a nonblocking Unix datagram pair and
poll(2). It is enabled only after one connection becomes multiplexed; the single-stream read path remains unchanged. - In controlled Linux A-B-B-A runs, c8 and c32 throughput improved by about 21% and 15%, while p95 latency fell by about 12% on one H2 connection. The c1 result remained within normal measurement noise.
- Corrected the H2 wake drain transition on Windows and Linux so concurrent notifications cannot suppress a later actor wakeup.
- Added a same-run Linux wheel gate for c1, c8, c32, and c128 H2 workloads. The gate requires successful responses, one physical H2 connection, and conservative throughput floors before artifact upload.
- No built-in ClientHello, TLS record, HTTP/2 frame, HPACK, header-order, or profile wire configuration changed in this release.
What's new in 2.16.2
- Replaced the Linux H3 actor's fixed UDP polling delay with a nonblocking
Unix datagram wake channel and
poll(2)readiness wait. The wake is enabled only while at least two H3 streams are active; the single-stream path is unchanged. - Fixed a Linux multi-stream failure in which the published 2.16.1 wheel could lose the bounded c8 localhost fixture with a connected-UDP refusal. The candidate completed c8, c32, and c128 on one reused QUIC connection.
- Corrected the Windows and Linux wake-drain state transition so a concurrent notification cannot leave later actor wakeups suppressed.
- Added a Linux wheel CI gate that runs bounded c8, c32, and c128 H3 workloads, requires one QUIC connection, and rejects severe throughput regressions before artifact upload.
- No built-in ClientHello, QUIC packet, QPACK, header-order, or profile wire configuration changed in this release.
What's new in 2.16.1
- Completed the post-2.16 Requests compatibility audit. Cookie containers now
include detached
copy(), legacy iterator helpers, session/expired cleanup, and requests-compatiblemultiple_domains()behavior. - Added mutable
Session.paramsandSession.max_redirects, including async and shared/isolated pool behavior. Request mappings override Session query defaults and remove defaults withNone. - Added a Windows multi-stream H3 actor wake path without changing the QUIC UDP polling path for c1 or Linux. Controlled A-B-B-A runs kept c1 unchanged and improved H3 concurrency-32 throughput by about 4% and p95 by about 7%.
- Prevented internal H2/H3 wake sockets from accumulating TIME_WAIT state and exhausting Windows ephemeral ports. The proxy suite passed five consecutive rounds after the fix.
- Revalidated Chrome 149/150 H3 wire behavior with three new native runs per profile; both remain equivalent to the stored device samples. Chrome and Cronet cold connections, warm reuse, and TLS resumption passed.
- Repaired the H3 helper's complete 30-profile match and the installed-native evidence contract, which still expected the older 22-profile set.
- Removed the stale 2.4.0 repository-snapshot path from the advanced wheel and
publish workflows. Both now require an exact version tag and matching
release/X.Y.Z-repository-snapshot.jsoninput.
What's new in 2.16.0
- Added a unified regression catalog covering every built-in profile, TLS family, and HTTP family without merging unrelated transports into one claim.
- Added reproducible cold and warm performance gates for the Windows/Linux binary workflow, with hash-pinned CMake, libclang, NASM, and Visual Studio generator selection.
- Reduced Windows HTTP/2 actor queue latency with an adaptive, lazily created wake channel. In the controlled 10 ms local fixture, concurrency 8 and 32 improved by approximately 87% and 78% while retaining one H2 connection.
- Made
session.cookiesandresponse.cookiesiteration match requests:for cookie in jarnow yieldshttp.cookiejar.Cookieobjects with scoped name, value, domain, path, security, expiry, and extension metadata. - Added opt-in native H2 stage diagnostics for local performance development; normal release wheels do not compile or expose the diagnostic counters.
- No built-in TLS ClientHello, ALPN, HTTP header ordering, HPACK, QPACK, or request write-plan profile was changed in this release.
What's new in 2.15.2
- Completed a second packet-level audit of the Android platform HttpsURLConnection profile and all four bundled Conscrypt profiles.
- Corrected the complete HelloRetryRequest lifecycle: the platform and 2.5 family retain their captured padded second ClientHello, while the 2.6 family retains its shorter unpadded form.
- Corrected buffered HTTP/1.1 request-body TLS writes. Small bodies coalesce with the request head; larger bodies use the captured 8 KiB plaintext boundaries and a separate remainder.
- Hardened four-way connection comparison, public-port PCAP isolation, final TLS close observation, and eager Conscrypt session shutdown.
- Added three-run device/native gates for seven body sizes, plus refreshed direct, HTTP CONNECT, certificate-failure, reuse, reconnect, method, TLS 1.2, and concurrency evidence.
What's new in 2.15.1
- Completed a second packet-level audit of all three Android Ktor profiles, including the full HelloRetryRequest ClientHello lifecycle, forced H1, negotiated H2, HTTP CONNECT, certificate policy, and buffered bodies from one through 32,769 bytes.
- Corrected CIO, Android URLConnection, and OkHttp TLS write boundaries for small and large H1 request bodies.
- Corrected Ktor OkHttp H2 DATA framing, early-response GOAWAY/RST_STREAM behavior, connection retirement, and cold concurrent candidate connections.
- Expanded the device/native acceptance gate to compare complete ClientHello lifecycles, TLS record shapes, H1 plaintext request hashes, and H2 frame behavior.
What's new in 2.15.0
- Added
ktor_android_cio_3.5.2,ktor_android_urlconnection_3.5.2, andktor_android_okhttp_3.5.2from three dependency-isolated release APKs. - Bound three process-cold ClientHellos per engine to both controlled server bytes and Wireshark PCAP bytes.
- Added Ktor-specific H1/H2 default headers, method/body ordering, connection lifecycle, TLS 1.2, TLS 1.3 capability/HRR, IP SNI, certificate policy, and HTTP CONNECT behavior.
- Added a CIO-specific TLS 1.2 ClientHello implementation. The Ktor Android and OkHttp profiles reuse their real underlying TLS families while preserving Ktor's distinct request headers.
What's new in 2.14.0
- Added
httpsurlconnection_android_api30for the Android 11 platformAndroidOpenSSLprovider. - Added independently built and captured Conscrypt 2.5.2, 2.5.3, 2.6.0, and 2.6.3 profiles.
- Bound three process-cold ClientHellos per profile to both a controlled TLS fixture and Wireshark PCAP bytes. The captures establish separate 2.5 and 2.6 supported-version families even though their JA3 and JA4 values match.
- Verified HTTP/1.1 Header order and casing, same-connection reuse, TLS session resumption, four-way concurrency, TLS 1.2, TLS 1.3 HelloRetryRequest, IP-literal SNI omission, hostname rejection, and HTTP CONNECT behavior.
What's new in 2.13.0
- Added five physical-device-verified Flutter iOS profiles covering Flutter 3.35.7, 3.38.10, 3.41.9, 3.44.9, and 3.47.1.
- Pinned each profile to its exact Dart runtime and verified six cold ClientHello samples per version across dart:io and Dio.
- Verified iOS AES-first cipher preference, TLS 1.2, TLS 1.3 HelloRetryRequest, certificate rejection, HTTP/1.1 Header order, connection reuse, the 15-second idle boundary, and four-way concurrency.
- Added bounded Mac automation for SDK validation, Release app signing, RVI packet capture, TLS lifecycle capture, and behavior collection.
What's new in 2.12.0
- Completed packet-level audits for Flutter Android 3.47.1 and every WebView profile from 140 through 150. WebView coverage includes main documents, same-origin and cross-origin frames, scripts, styles, images, fetch, XHR, CORS preflight, and request bodies over HTTP/1.1 and HTTP/2.
- Added separate Android Chromium ordering policies for navigation, ordinary subresources, same-site and cross-site fetch, preflight, and HTTP/3 fetch.
- Confirmed with three cold device and three native runs per Chrome version
that ordinary HTTP/3 fetch does not contain
PragmaorCache-Control. Chrome and WebView profile defaults never synthesize either field. - Corrected WebView H2 priority behavior, IP-host SNI handling, HTTP/1.1
content-typecasing, Chrome caller-only Header ordering, and OkHttp's generated HTTP/1.1Content-Typecasing. - Hardened incremental response decoding, streaming uploads and downloads, H2 reader fairness, GOAWAY handling, cancellation, and Windows pending-socket handling.
- Revalidated Chrome 149/150 H3 wire shape and six connection lifecycle scenarios, plus the complete OkHttp 4.12 and 5.4 behavior matrices.
- Expanded the Requests-style surface, including
Session.request(), response cookies, mutable session cookies, streaming readers, async sessions, and HTTP/SOCKS5 proxy routing.
Custom TLS/H1/H2 configurations remain isolated from built-in profiles. Custom HTTP/3 is not supported.
Installation
python -m pip install rex-tls
Requirements:
| Item | Supported value |
|---|---|
| Python | CPython 3.9 or newer |
| Windows | x86-64 |
| Linux | x86-64, manylinux 2.28 or newer |
The package is distributed as binary wheels. Installation on an unsupported platform fails instead of silently building a different native core from a source distribution.
Transport profiles
| Profile | HTTP/1.1 | HTTP/2 | HTTP/3 | Automatic response decoding |
|---|---|---|---|---|
chrome_android_149 |
Yes | Yes | Yes | gzip, deflate, Brotli, zstd, stacked codings |
chrome_android_150 |
Yes | Yes | Yes | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_140 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_141 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_142 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_143 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_144 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_145 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_146 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_147 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_148 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_149 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
webview_android_150 |
Yes | Yes | No | gzip, deflate, Brotli, zstd, stacked codings |
cronet_android_151 |
Yes | Yes | Yes | gzip, deflate, Brotli, zstd, stacked codings |
okhttp_4.12 |
Yes | Yes | No | gzip |
okhttp_5.4 |
Yes | Yes | No | gzip |
flutter_android_3.47.1 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
flutter_ios_3.35.7 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
flutter_ios_3.38.10 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
flutter_ios_3.41.9 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
flutter_ios_3.44.9 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
flutter_ios_3.47.1 |
Yes | No | No | one exact lowercase gzip value; representation Headers remain visible |
httpsurlconnection_android_api30 |
Yes | No | No | gzip |
conscrypt_android_2.5.2 |
Yes | No | No | gzip |
conscrypt_android_2.5.3 |
Yes | No | No | gzip |
conscrypt_android_2.6.0 |
Yes | No | No | gzip |
conscrypt_android_2.6.3 |
Yes | No | No | gzip |
ktor_android_cio_3.5.2 |
Yes | No | No | caller-selected decoding; CIO sends no default Accept-Encoding |
ktor_android_urlconnection_3.5.2 |
Yes | No | No | gzip |
ktor_android_okhttp_3.5.2 |
Yes | Yes | No | gzip |
Aliases are explicit mappings:
| Alias | Selected profile |
|---|---|
chrome_android |
chrome_android_150 |
chrome_android_latest |
chrome_android_150 |
cronet |
cronet_android_151 |
cronet_latest |
cronet_android_151 |
okhttp |
okhttp_5.4 |
okhttp_latest |
okhttp_5.4 |
flutter |
flutter_android_3.47.1 |
flutter_latest |
flutter_android_3.47.1 |
Use a versioned name when an application must remain pinned to one profile. The profiles cover application-layer TLS and HTTP behavior. Operating-system IP and TCP packet fields are produced by the machine running rex-tls.
Choose the profile that matches the networking component used by the original client:
- Use
chrome_android_149orchrome_android_150for Android Chrome browser traffic. - Use
cronet_android_151for an Android app that sends requests through the packaged Cronet engine. - Use
webview_android_140throughwebview_android_150for the matching WebView provider. The request policies cover main documents, frames, subresources, fetch/XHR, CORS, and request bodies. - Use
okhttp_4.12orokhttp_5.4for an Android app that sends requests through that OkHttp version. - Use
flutter_android_3.47.1for a Flutter Android app that uses the defaultdart:io HttpClient, including Dio with its default IO adapter. - Use
flutter_ios_3.35.7,flutter_ios_3.38.10,flutter_ios_3.41.9,flutter_ios_3.44.9, orflutter_ios_3.47.1for the matching Flutter iOS transport. These are not aliases of the Android profile. - Use
httpsurlconnection_android_api30for the Android 11 platform-default HttpsURLConnection stack. Use a versionedconscrypt_android_*profile only when the application explicitly installs that Conscrypt AAR as its TLS provider. - Use
ktor_android_cio_3.5.2,ktor_android_urlconnection_3.5.2, orktor_android_okhttp_3.5.2for a Ktor 3.5.2 application using that exact engine. The engine name matters: Ktor does not have one shared TLS stack. - Use
profile="custom"only when the caller owns and validates a separate TLS/H1/H2 definition. It does not identify itself as a built-in profile.
Android Ktor 3.5.2
import rex_tls
with rex_tls.Session("ktor_android_cio_3.5.2", timeout=20) as session:
response = session.get("https://example.com/api")
with rex_tls.Session("ktor_android_okhttp_3.5.2", http2=True) as session:
response = session.get("https://example.com/api")
ktor_android_cio_3.5.2 is an independent TLS 1.2 and HTTP/1.1 transport with
no ALPN. ktor_android_urlconnection_3.5.2 uses the Android platform TLS
family and Ktor's distinct H1 headers. ktor_android_okhttp_3.5.2 resolves
OkHttp 5.3.2, uses the audited OkHttp Android 5.x ClientHello/H2 settings, and
adds Ktor's accept and user-agent fields before OkHttp's
accept-encoding.
For four simultaneous cold calls, both the device engine and rex-tls open four candidate TLS connections but carry all requests on one H2 connection. Losing candidates flush the same H2 prelude and close without carrying a request. A stale reused connection may be retried once only for a bodyless GET or HEAD.
All three support the normal HTTP and SOCKS5 proxy arguments. None has an
audited HTTP/3 path. http2=True is accepted only for the OkHttp engine.
The device CIO implementation sent an IP literal in SNI and accepted a
certificate-name mismatch. rex-tls reproduces the SNI shape but keeps
verify=True as its security default. Detailed evidence and method-specific
header tables are in the Android Ktor profile document.
Flutter Android 3.47.1
import rex_tls
with rex_tls.Session("flutter_android_3.47.1", timeout=20) as session:
response = session.get("https://example.com/api")
print(response)
print(response.http_version)
This profile is pinned to Flutter 3.47.1 and Dart 3.13.1. The Android release
probe used dart:io HttpClient; Dio 5.11.0 with its default
IOHttpClientAdapter produced the same stable TLS and Header behavior.
The real client sends no ALPN and therefore uses HTTP/1.1. The profile does not claim H2 or H3,
so http2=True and http3="only" are rejected. Its process-cold, warm-process,
forced-new-connection, and same-connection-reuse paths were collected
separately. New TLS connections remain cold and do not add a PSK extension;
an open HTTP/1.1 connection is reused by sequential requests.
With no caller Headers, the lowercase wire order is user-agent,
accept-encoding, then host. Dart's defaults remain present when application
Headers are supplied. Ordering uses the complete Dart 3.13 Android AOT
HashMap mutation history rather than Python dictionary order, including
hidden framing fields and resize boundaries. HTTP CONNECT uses the same
Android AOT HashMap behavior, and the TLS ClientHello inside the tunnel is
identical to the direct path.
Only one case-sensitive Content-Encoding value exactly equal to gzip is
decoded automatically. Uppercase, comma-list, Brotli, deflate, zstd, and
unknown values remain encoded; repeated field lines fail. Flutter keeps the
original Content-Encoding and Content-Length fields visible after decoding.
Buffered and streaming paths also match Dart's truncated-gzip and checksum
behavior. Detailed verified behavior and the evidence definition are in
the Flutter Android profile document.
The profile uses Dart's 15-second idle lifetime. When
max_connections_per_route is omitted, it also follows Dart's default
uncapped per-host policy; pass an integer to apply an explicit bound.
Flutter iOS 3.35.7 through 3.47.1
import rex_tls
with rex_tls.Session("flutter_ios_3.47.1", timeout=20) as session:
response = session.get("https://example.com/api")
print(response)
print(response.http_version)
The versioned iOS profiles cover Flutter 3.35.7 / Dart 3.9.2, Flutter 3.38.10 /
Dart 3.10.9, Flutter 3.41.9 / Dart 3.11.5, Flutter 3.44.9 / Dart 3.12.2, and
Flutter 3.47.1 / Dart 3.13.1. All use the default dart:io HttpClient. Dio
5.11.0 with IOHttpClientAdapter produces the same ClientHello and HTTP/1.1
transport behavior in every captured version.
The iOS and Android profiles share the Dart extension order, supported groups, signature algorithms, lack of ALPN, HTTP/1.1 Header rules, 15-second idle boundary, and response decoding. They are still separate because iOS prefers AES-GCM cipher suites before ChaCha20 and therefore has a different JA3.
Each version passed three cold-process dart:io samples. It also passed three cold-process Dio samples, four concurrent connections, TLS 1.2, TLS 1.3 HelloRetryRequest, certificate rejection, direct reuse, the 14/16-second idle boundary, and a service-side HTTP/1.1 Header check. The full contracts are in the Flutter iOS profile document and the five-version matrix.
The generic aliases flutter and flutter_latest continue to select the
Android profile for backward compatibility. Select flutter_ios_3.47.1
explicitly for iOS.
Android HttpsURLConnection and Conscrypt
import rex_tls
with rex_tls.Session("httpsurlconnection_android_api30", timeout=20) as session:
response = session.get("https://example.com/api")
print(response)
print(response.http_version) # HTTP/1.1
The bundled-provider profiles are selected explicitly:
with rex_tls.Session("conscrypt_android_2.6.3") as session:
response = session.get("https://example.com/api")
The platform profile and Conscrypt 2.5.2/2.5.3 advertise TLS 1.3 through TLS
1.0. Conscrypt 2.6.0/2.6.3 advertise TLS 1.3 and TLS 1.2. Cipher order,
extension order, signature algorithms, groups, key share, and HTTP/1.1 ALPN
are otherwise equal in the verified cold captures. The profiles do not offer
HTTP/2 or HTTP/3, so http2=True and http3="only" are rejected.
With empty caller Headers, the H1 order is User-Agent, Host, Connection,
then Accept-Encoding. A known-length body places Content-Type and
Content-Length first. The real Dalvik User-Agent contains device/build data;
rex-tls uses a privacy-neutral Android 11 value by default. Pass the original
application's complete User-Agent when exact application Header values are
required.
The default pool permits five same-route connections. Sequential requests
reuse one connection; a forced second connection can resume TLS 1.3. HTTP
CONNECT preserves the direct ClientHello and sends CONNECT Headers in the
verified Host, Proxy-Connection, User-Agent order. Detailed evidence and
the profile boundary are documented in
the Android Conscrypt profile document.
Cronet 151
Use the exact versioned name when HTTP/3 must be required:
import rex_tls
with rex_tls.Session(
profile="cronet_android_151",
http3="only",
) as session:
response = session.get(
"https://example.com/api",
headers={
"X-Application-Id": "example",
"User-Agent": "ExampleApp/1.0 Cronet/151.0.7922.83",
},
)
print(response.http_version)
print(response.connection_reused)
Connection reuse is exposed through Response.connection_reused.
Cronet is an application networking library, not the Android Chrome browser or Android WebView. Its profile therefore has its own default Headers and priority behavior. H1, H2, and H3 share the same Session cookie jar and connection lifecycle, while each protocol retains its audited wire order.
Android WebView 140 through 150
import rex_tls
with rex_tls.Session(profile="webview_android_144") as session:
response = session.get("https://example.com/")
print(response.http_version)
WebView 140 through 149 have independently collected baselines with the same
stable TLS fields. WebView 150 has its own signature-algorithm change. None of
the profiles uses the Android Chrome or Cronet Header table. Their default
navigation omits Chrome Client Hints and uses the WebView navigation order.
The exact User-Agent, Accept-Language, and X-Requested-With values depend on
the embedding app and runtime.
When headers is empty, rex-tls supplies the stable WebView navigation
defaults but does not invent an application package for X-Requested-With.
When headers is non-empty, the existing exact-header contract applies:
rex-tls sorts only the fields supplied by the caller and does not add profile
defaults. Pass the complete app Header set when an application-specific
X-Requested-With value is required.
Controlled navigation for every supported WebView version did not attempt QUIC
after authenticated Alt-Svc across three- and four-navigation sequences, so
these profiles do not claim HTTP/3 support. http3="only" is rejected.
The per-version evidence definition and usage notes are in
the Android WebView profile document.
Caller-defined TLS and HTTP/2 profiles
Use profile="custom" when the wire configuration comes from your own
tls-client or requests-go definition. A custom configuration is separate from
all built-in profiles and does not claim to represent any particular
browser, application, or device.
Import a tls-client configuration
import rex_tls
tls_client_config = {
"ja3String": (
"771,4865-4866-4867-49195-49199,"
"0-10-11-13-16-18-27-43-45-51-17613-65037,"
"4588-29-23-24,0"
),
"supportedSignatureAlgorithms": [
"ecdsa_secp256r1_sha256",
"rsa_pss_rsae_sha256",
"rsa_pkcs1_sha256",
"ecdsa_secp384r1_sha384",
"rsa_pss_rsae_sha384",
"rsa_pkcs1_sha384",
"rsa_pss_rsae_sha512",
"rsa_pkcs1_sha512",
],
"supportedVersions": ["GREASE", "1.3", "1.2"],
"keyShareCurves": ["GREASE", "X25519MLKEM768", "X25519"],
"alpnProtocols": ["h2", "http/1.1"],
"alpsProtocols": ["h2"],
"certCompressionAlgos": ["brotli"],
"h2Settings": {
"HEADER_TABLE_SIZE": 65536,
"ENABLE_PUSH": 0,
"INITIAL_WINDOW_SIZE": 6291456,
},
"h2SettingsOrder": [
"HEADER_TABLE_SIZE",
"ENABLE_PUSH",
"INITIAL_WINDOW_SIZE",
],
"connectionFlow": 15663105,
"streamId": 1,
"pseudoHeaderOrder": [":method", ":authority", ":scheme", ":path"],
"headerPriority": {
"streamDep": 0,
"exclusive": True,
"weight": 255,
},
}
config = rex_tls.CustomTLSConfig.from_tls_client(
tls_client_config,
header_order=["accept", "host", "user-agent", "x-request-id"],
)
with rex_tls.Session(
profile="custom",
tls_config=config,
http2=True,
) as session:
response = session.get(
"https://example.com/",
headers={
"X-Request-Id": "example",
"User-Agent": "caller-owned-agent",
"Accept": "*/*",
},
)
print(response.http_version)
tls-client stores an HTTP/2 priority weight as its encoded byte. The value
255 in headerPriority therefore represents the displayed HTTP/2 weight
256. CustomTLSConfig performs that conversion automatically.
Import a requests-go configuration
import rex_tls
config = rex_tls.CustomTLSConfig.from_requests_go(requests_go_config)
with rex_tls.Session(profile="custom", tls_config=config) as session:
response = session.request("GET", "https://example.com/")
from_requests_go() accepts a requests-go TLSConfig mapping or a stored
capture document containing tls and http2 sections. It converts the
requests-go HTTP/2 weight representation and preserves its extension,
SETTINGS, pseudo-header, and ordinary Header order.
Custom-profile rules
profile="custom"requires aCustomTLSConfig. Passingtls_configto a built-in profile raises an error.- Custom profile v1 accepts HTTPS URLs only;
allowHttp=Trueis rejected. - Accepted custom fields are emitted by the project-owned TLS, HTTP/1.1, and HTTP/2 paths. Unknown or unsupported fields are rejected.
- The configuration is immutable.
config.identifieris derived from the normalized wire configuration, andSession.profilereports a shortcustom:identifier. http2=Truerequiresh2in ALPN and extension 16 in the ClientHello extension list. It means that successful HTTP/2 negotiation is mandatory.- A custom profile supplies no browser or OkHttp default Headers. The request
contains the Headers provided by the caller plus protocol-required fields
such as
Host,Content-Length, or HTTP/2 pseudo-headers. header_ordersorts only fields that are present; it does not add or remove caller Headers. Unlisted fields keep their relative order after listed fields. Includehostwhen its HTTP/1.1 position matters.- Extension 41 may be specified only as the final extension. BoringSSL emits it only when a resumable TLS session supplies a pre-shared key.
- Custom HTTP/3, delegated credentials, record-size-limit configuration, caller-provided ECH payloads, and explicit padding extension placement are not supported by custom profile v1.
profiles()andprofile_info()describe only the built-in profiles. The caller is responsible for validating a custom configuration against its intended source.
Custom profiles work with AsyncSession, HTTP and SOCKS5 proxies, shared or
isolated pools, streaming requests, and streaming responses. Each pool member
uses the same immutable configuration while retaining its own connection and
cookie state according to the selected pool mode.
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)
Top-level request(), get(), post(), put(), patch(), delete(), head(), and options() create a temporary session. Reuse a Session when making more than one request to the same service.
import rex_tls
response = rex_tls.request(
"POST",
"https://api.example.com/items",
profile="okhttp_5.4",
json={"name": "example", "enabled": True},
)
Persistent sessions
A Session retains cookies, connections, protocol state, and TLS session tickets across requests.
Session.request(method, url, ...) is the generic request entry point. get(), post(), put(), patch(), delete(), head(), and options() are convenience wrappers around it.
import rex_tls
with rex_tls.Session(profile="chrome_android_150") as session:
response = session.request(
method="POST",
url="https://example.com/api/items",
params={"source": "python"},
headers={"accept": "application/json"},
cookies={"request-only": "value"},
json={"name": "example"},
timeout=20,
)
response.raise_for_status()
The example explicitly uses method="POST", url="https://example.com/api/items", and timeout=20.
The same Session can carry subsequent requests and expose connection reuse:
from rex_tls import Session
with Session("okhttp_5.4", timeout=20) as session:
session.headers.update({"accept": "application/json"})
session.params.update({"api-version": "2026-08"})
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())
connection_reused is False when a new connection was required and True
when the request used an existing connection. A server close, a different
origin or proxy route, incompatible protocol state, an unread streaming body,
or a connection limit can prevent reuse.
session.params supplies default query parameters for every request. A
request mapping overrides matching Session keys and removes a default when its
value is None. An ordered request sequence is used as-is, matching requests.
session.max_redirects is readable and may be changed after construction.
Session options
session = Session(
profile="chrome_android_150",
timeout=30,
verify=True,
proxy=None,
proxies=None,
follow_redirects=True,
max_redirects=10,
trust_env=False,
http3="off",
http2=False,
max_connections_per_route=None,
tls_config=None,
)
| Option | Meaning |
|---|---|
profile |
Transport profile or explicit alias. |
timeout |
Default timeout number or (connect, read) pair. |
verify |
True, False, or a CA bundle path. |
proxy |
One proxy URL for all routes. Mutually exclusive with proxies. |
proxies |
Requests-style route mapping. Mutually exclusive with proxy. |
follow_redirects |
Session default for redirect following. |
max_redirects |
Maximum redirects in one request chain. |
trust_env |
Read standard proxy environment variables when True. |
http3 |
"off", "auto", or "only". |
http2 |
Require actual HTTP/2 negotiation when True. |
max_connections_per_route |
Maximum HTTP/1.1 connections per origin and proxy route. None selects the profile default: uncapped for Flutter and 1 for the other built-in profiles. |
tls_config |
Required immutable CustomTLSConfig when profile="custom"; rejected for built-in profiles. |
Request arguments
Session.request() and every method helper accept the following request options:
| Argument | Meaning |
|---|---|
params |
Query mapping or ordered sequence of pairs. |
headers |
Header mapping or ordered sequence of name/value pairs. |
cookies |
Cookies for this request only; the session jar is not mutated. |
content |
Bytes, text, file-like object, or byte iterable. |
data |
Form fields, bytes, text, file-like object, or byte iterable. |
json |
JSON-serializable value. |
files |
Requests-style multipart file mapping. |
decode_content |
Decode supported Content-Encoding values when True. |
timeout |
Override the session timeout for this request. |
proxies |
Override selected entries in Session.proxies for this request. |
stream |
Return before downloading the full body when True. |
allow_redirects |
Override the session redirect policy for this request. |
navigation_site |
Chrome request context for profile header selection. |
user_activation |
Chrome request context for profile header selection. |
content, data, and json are mutually exclusive. files may be combined
with form fields in data, but not with content or json.
Timeouts and redirects
A single timeout value applies one absolute request deadline and the same value to each TCP socket phase:
response = session.get("https://example.com/", timeout=20)
A pair separates TCP connect and socket read/write limits. Its sum is the absolute deadline for the complete redirect chain:
response = session.get("https://example.com/", timeout=(3, 10))
The split form is timeout=(3, 10).
HTTP/3 uses the combined absolute deadline because QUIC connection progress is managed by one protocol driver.
Disable redirects per request when the caller wants to inspect the next hop:
response = session.get(
"https://example.com/redirect",
allow_redirects=False,
)
print(response.is_redirect)
print(response.next)
response.history contains followed redirect responses from oldest to newest.
For 307 and 308 redirects, seekable upload sources are rewound to their original
position. A one-shot source raises UnrewindableBodyError when replay is
required.
Request headers and wire order
Headers can be a mapping or an ordered list of pairs:
headers = [
("Accept", "application/json"),
("X-Trace", "one"),
("X-Trace", "two"),
]
response = session.get("https://example.com/api", headers=headers)
The native core prepares headers using the selected profile and negotiated protocol:
- When both Session.headers and request headers are empty, the selected profile supplies its default headers.
- When the caller supplies headers, rex-tls does not add unrelated profile defaults. It can still generate fields required by the URL, cookies, body, or wire protocol.
- Chrome and WebView defaults never add
PragmaorCache-Control. If a caller explicitly supplies either field, rex-tls preserves it and only places it in the selected request-class order. - Cronet always retains its transport-managed
User-Agent,Accept-Encoding, and H2/H3priorityfields when application Headers are present, matching the pinned Cronet engine. Supplying one of those names replaces its value. - Chrome profiles sort known fields with protocol-specific navigation and fetch order tables. Unknown fields keep their caller-relative order after known fields.
- The WebView profile uses its independent navigation table. It never adds an
application-specific
X-Requested-Withvalue on the caller's behalf. - OkHttp profiles preserve caller field order and apply only the casing required by the selected wire protocol.
- HTTP/1.1 uses profile casing for recognized fields. Unknown HTTP/1.1 names keep the caller's spelling.
- HTTP/2 and HTTP/3 send ordinary field names in lowercase. Pseudo headers are
generated by the native core and are not supplied in
headers.
PC Chrome Header order with Android TLS
The versioned Android Chrome profiles can intentionally combine their Android TLS/HTTP transport profile with PC Chrome ordinary Header order. The switch is automatic when the caller explicitly supplies a desktop Chromium User-Agent:
import rex_tls
pc_headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/151.0.0.0 Safari/537.36"
),
"sec-ch-ua-platform": '"Windows"',
"sec-ch-ua": '"Google Chrome";v="151", "Chromium";v="151"',
"sec-ch-ua-mobile": "?0",
"Content-Type": "application/json",
"Accept": "*/*",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
}
with rex_tls.Session("chrome_android_150", http2=True) as session:
response = session.post(
"https://example.com/api",
headers=pc_headers,
json={"enabled": True},
)
print(response.request.headers.raw)
Selection rules:
- The User-Agent must contain
Chrome/,Chromium/, orEdg/and a desktop platform marker such asWindows NT,Macintosh,X11;, orCrOS. Android,iPhone,iPad,Mobile, orTabletkeeps the Android order.- An empty Header set still uses the Android profile defaults.
- Only fields already present are sorted. The mode does not add PC browser defaults or remove caller fields.
- TLS ClientHello, ALPN, H2/H3 SETTINGS, priorities, connection pools, and
Session.profileremain the selected Android Chrome profile. - The branch applies only to
chrome_android_149andchrome_android_150; Cronet, OkHttp, and custom profiles are unchanged.
PC Chrome 150 and 151 produced the same observed fetch order over H1 and H2. The selected Android TLS, ALPN, H2/H3 settings, and connection behavior remain unchanged when desktop Header ordering is selected.
Navigation versus fetch requests
Chrome H2 selects its legacy HEADERS priority from the prepared request:
- Empty Session and request headers use the complete navigation defaults and weight 256.
Sec-Fetch-Mode: navigate,Sec-Fetch-Dest: document, orUpgrade-Insecure-Requests: 1marks an explicit navigation request and uses weight 256.- Other explicit header sets are treated as fetch/API requests and use weight
220. Typical signals are
Sec-Fetch-Mode: corsandSec-Fetch-Dest: empty.
# Top-level document navigation.
navigation_headers = {
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
"Upgrade-Insecure-Requests": "1",
"Accept": "text/html,application/xhtml+xml",
}
# JSON API request made with fetch semantics.
fetch_headers = {
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Content-Type": "application/json",
"Accept": "*/*",
}
Supplying only ordinary fields such as User-Agent and Accept, without a
navigation signal, selects fetch behavior. navigation_site changes the
Sec-Fetch-Site value in generated Chrome navigation defaults; it does not
convert an explicit fetch header set into navigation headers.
Do not copy an HTTP/1.1 header block unchanged into HTTP/2 or HTTP/3.
Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding, and
Upgrade are connection-specific and are rejected. TE is valid only with
the value trailers. For HTTP/3, omit Host; authority comes from the request
URL.
For example, this is valid for HTTP/1.1:
h1_headers = {
"Host": "www.example.com",
"Connection": "keep-alive",
"Accept": "application/json",
"User-Agent": "my-client/1.0",
}
For a forced HTTP/2 request, use ordinary end-to-end headers only:
h2_headers = {
"accept": "application/json",
"user-agent": "my-client/1.0",
}
with Session("chrome_android_150", http2=True) as session:
response = session.get("https://www.example.com/", headers=h2_headers)
The complete H1, H2, and H3 header contract is documented in the header guide.
Cookies
Session.cookies is a mutable Requests-style CookieJar view backed by the native session:
from rex_tls import Session
with Session("okhttp_5.4") as session:
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())
for cookie in session.cookies:
print(cookie.name, cookie.value, cookie.domain, cookie.path)
session.cookies.clear(
domain="api.example.com",
path="/v1",
name="api-token",
)
Available CookieJar operations:
| Method | Behavior |
|---|---|
set(name, value, ...) |
Create, replace, or remove one cookie identity. |
set_cookie(cookie) |
Add an http.cookiejar.Cookie object. |
get(name, ...) |
Read one cookie, optionally scoped by domain and path. |
get_dict(...) |
Return matching cookies as a dictionary. |
update(values) |
Merge a mapping or another cookie container. |
copy() |
Return a detached scoped CookieJar copy. |
clear(...) |
Remove one cookie, a scoped group, or the complete jar. |
clear_session_cookies() |
Remove cookies without a persistent expiry. |
clear_expired_cookies() |
Remove cookies whose expiry has elapsed. |
keys(), values(), items() |
Inspect stored cookies. |
iterkeys(), itervalues(), iteritems() |
Return requests-compatible iterators. |
for cookie in jar |
Iterate http.cookiejar.Cookie objects with full scope and metadata. |
list_domains(), list_paths() |
Inspect stored scopes. |
A valid response cookie with Domain=example.com is accepted when the response
host is example.com or a subdomain such as api.example.com. A cookie is
rejected when its Domain does not match the response host or is a public suffix
such as com.
Response and Session cookie containers have different scopes:
with Session("okhttp_5.4") as session:
response = session.get("https://example.com/login")
# Cookies accepted from this response only.
print(response.cookies.get_dict())
# All cookies retained for later requests.
print(session.cookies.get_dict())
Both containers expose the common methods above. Mutating response.cookies
does not mutate session.cookies. Cookie storage evaluates Domain, Path,
Secure, expiry, deletion, prefix rules, IP-address rules, HttpOnly, SameSite,
and the built-in public-suffix list.
An unscoped get(name) raises CookieConflictError when multiple stored
domain/path identities have the same name. Supply domain and path to make
the lookup unambiguous.
HTTP and SOCKS5 proxies
Use proxy when one proxy handles every route:
proxy_url = "http://username:password@proxy.example:8080"
with Session("okhttp_5.4", proxy=proxy_url) as session:
response = session.get("https://example.com/")
Use proxies for per-scheme routing and bypass rules:
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/")
Proxy selection uses this order:
- A per-request
proxiesentry updates the Session.proxies mapping for that request. - The target scheme key,
httporhttps, takes priority overall. no_proxybypasses the selected proxy.- When
trust_env=True, unset routes may useHTTP_PROXY,HTTPS_PROXY,ALL_PROXY, andNO_PROXY.
Set an explicit route value to None to disable that route. proxy and
proxies cannot be passed together to the Session constructor.
HTTP proxy URLs use the http:// scheme. HTTP destinations use absolute-form
requests; HTTPS destinations use HTTP CONNECT. Basic proxy credentials are
supported. Percent-encode reserved characters in usernames and passwords.
SOCKS5 uses the same arguments:
# Local destination DNS resolution.
with Session("okhttp_5.4", proxy="socks5://127.0.0.1:1080") as session:
response = session.get("https://example.com/")
# Proxy-side destination DNS resolution and username/password authentication.
with Session(
"chrome_android_150",
proxy="socks5h://username:password@proxy.example:1080",
) as session:
response = session.get("https://example.com/")
| Scheme | Destination DNS |
|---|---|
socks5:// |
Resolved by the rex-tls host before SOCKS CONNECT. |
socks5h:// |
Hostname sent to the proxy for resolution. |
IPv4 and IPv6 literals are always sent as literal addresses. SOCKS5 itself does not encrypt proxy credentials, so use it over a trusted network path.
HTTP CONNECT and SOCKS5 establish a tunnel first. The selected rex-tls TLS ClientHello, ALPN, and H1/H2 behavior are then generated inside that tunnel; the proxy type does not replace the selected TLS profile.
Supported proxy schemes are http://, socks5://, and socks5h://. HTTPS
proxy URLs, SOCKS4, PAC, NTLM/Digest proxy authentication, and MASQUE are not
implemented.
HTTP version selection
The negotiated protocol is available as response.http_version.
Require HTTP/2
with Session("chrome_android_150", http2=True) as session:
response = session.get("https://example.com/")
assert response.http_version == "HTTP/2"
http2=True requires HTTPS and an actual HTTP/2 ALPN result. The request fails
if the server selects HTTP/1.1. It cannot be combined with an HTTP/3 mode.
Select HTTP/3
HTTP/3 is available for the exact versioned Chrome and Cronet profiles:
| Value | Behavior |
|---|---|
http3="off" |
Disable HTTP/3. This is the default. |
http3="auto" |
Learn authenticated Alt-Svc and use H3 when available, otherwise use H2/H1. |
http3="only" |
Require HTTP/3 and fail when it cannot be established. |
with Session("chrome_android_150", http3="auto") as session:
first = session.get("https://example.com/")
second = session.get("https://example.com/")
print(first.http_version, second.http_version)
with Session("chrome_android_149", http3="only") as session:
response = session.get("https://example.com/")
assert response.http_version == "HTTP/3"
with Session("cronet_android_151", http3="only") as session:
response = session.get("https://example.com/")
assert response.http_version == "HTTP/3"
In auto mode, the first request to an origin normally uses H2 or H1 so rex-tls can authenticate and learn the origin's Alt-Svc advertisement. A later request may use H3. Auto mode does not mean the first request is forced onto QUIC.
HTTP/3 requires HTTPS. OkHttp and WebView profiles do not offer H3. HTTP CONNECT and SOCKS5
are TCP proxy mechanisms, so http3="only" is rejected when a proxy is
selected. Auto mode uses the proxied H2/H1 path.
Streaming downloads and content decoding
Set stream=True and close the response after use. A context manager releases
the connection 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(chunk_size=64 * 1024):
output.write(chunk)
Streaming readers:
iter_content(chunk_size=..., decode_unicode=False)yields body chunks.iter_lines(chunk_size=..., decode_unicode=False, delimiter=None)yields complete lines.raw.read(size)andraw.readinto(buffer)provide file-like byte access.- Accessing
contentconsumes and caches an unread streaming body. close()releases the response without reading the remaining body.
Automatic decoding depends on the profile:
- Chrome, WebView, and Cronet profiles decode identity, gzip, deflate, Brotli, zstd, and stacked codings in reverse application order.
- OkHttp profiles automatically decode only a single gzip coding.
- The Flutter profile decodes only one exact lowercase
gzipvalue, preserves representation Headers, and otherwise follows Dart's pass-through and error behavior. decode_content=Falsepreserves the compressed wire body.- Truncated, corrupt, or oversized decoded bodies raise
ContentDecodingError.
The decoder is incremental and does not buffer the entire decoded body before yielding chunks.
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,
)
Multipart form uploads use files:
with Session("okhttp_5.4") as session:
with open("image.png", "rb") as source:
response = session.post(
"https://example.com/form",
data={"title": "example"},
files={"file": ("image.png", source, "image/png")},
)
Known-length H1 bodies use Content-Length; unknown-length H1 bodies use chunked transfer. H2 and H3 bodies are streamed through their native flow-control paths. Seekable sources can be replayed across 307 or 308 redirects.
Async API
AsyncSession provides an asyncio interface while the native work runs outside the event-loop thread:
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())
AsyncSession accepts the same transport and request options as Session, plus
max_concurrency, which bounds active operations. It exposes request(), all
method helpers, cancel(), acancel(), close_origin(), aclose_origin(), close(),
and async context management. Its close() method is awaited.
For strict HTTP/2 sessions, ordinary buffered requests without an upload use a single bounded completion dispatcher. Session cookies, response cookies, decoding, cancellation, timeout accounting, and non-followed redirect metadata remain available. Requests requiring streaming, uploads, redirect following, request-specific proxy or Cookie mappings, query merging, navigation context, or split connect/read timeouts automatically use the compatibility executor. No option is silently removed.
Async streaming uses aiter_content(), aiter_lines(), and aclose():
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 provide a fixed number of independent native sessions. Waiting callers are served in FIFO order.
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 state modes:
| Mode | Headers | Proxies | Cookies | Connections and TLS state |
|---|---|---|---|---|
shared |
Shared | Shared | One thread-safe jar | Independent per member |
isolated |
Independent | Independent | Independent | Independent per member |
In isolated mode, pool.headers, pool.proxies, and pool.cookies raise an
error because no pool-wide value exists. Lease one member when several requests
must retain the same isolated state:
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")
Pool requests accept pool_timeout for the time spent waiting for a member.
The ordinary timeout continues to control network work. A streaming response
holds its pool member until the body is consumed or the response is closed.
AsyncSessionPool provides the same modes and uses async with pool.acquire()
for an isolated lease.
Response API
Response implements the commonly used public Requests response surface and adds native transport information.
Requests-style attributes
| Attribute | Value |
|---|---|
status_code |
Numeric HTTP status. |
reason |
HTTP reason phrase. |
url |
Final response URL. |
headers |
Case-insensitive response header mapping. |
content |
Response body as bytes. |
text |
Decoded response text. |
encoding |
Selected text encoding; it can be reassigned. |
apparent_encoding |
Detected fallback encoding. |
cookies |
Cookies accepted from this response only. |
elapsed |
Request duration as datetime.timedelta. |
history |
Followed redirect responses, oldest first. |
request |
Prepared request metadata for the final hop. |
connection |
Owning Session transport reference. |
raw |
File-like body reader for streaming responses. |
ok |
Whether raise_for_status() would succeed. |
is_redirect |
Whether this response has a redirect target. |
is_permanent_redirect |
Whether this is a permanent redirect response. |
next |
Prepared follow-up request when redirects are disabled. |
links |
Parsed Link response-header relationships. |
Requests-style methods
| Method | Behavior |
|---|---|
json() |
Parse JSON or raise JSONDecodeError. |
raise_for_status() |
Raise HTTPError for 4xx and 5xx responses. |
iter_content() |
Iterate cached or streaming body chunks. |
iter_lines() |
Iterate cached or streaming body lines. |
close() |
Release response resources. |
Response supports boolean conversion and synchronous or asynchronous context
management. HTTPError and JSONDecodeError retain both response and
request context.
Printing or inspecting a response keeps the transport version visible:
print(response) # <Response [200 HTTP/2]>
Response.headers combines duplicate values for ordinary mapping access.
response.headers.get_all(name) returns every value, while
response.headers.raw preserves the received name/value order.
Prepared request metadata includes:
| Attribute | Value |
|---|---|
request.method |
Normalized HTTP method. |
request.url |
Full request URL. |
request.path_url |
Path and query. |
request.headers |
Ordinary headers prepared for that protocol hop. |
request.body |
Buffered request body when available. |
For H1, prepared headers include Host. For H2 and H3, names are lowercase and pseudo headers are represented by method, URL, path, and authority metadata rather than inserted into the ordinary header mapping.
rex-tls transport attributes
| Attribute | Value |
|---|---|
http_version |
HTTP/1.1, HTTP/2, or HTTP/3. |
elapsed_seconds |
Request duration as a floating-point number of seconds. |
local_address |
Local transport endpoint when available. |
remote_address |
Remote transport endpoint when available. |
connection_reused |
Whether an existing connection carried this request. |
tls_session_reused |
Whether the TLS handshake resumed an earlier session. |
closed |
Whether response resources are closed. |
consumed |
Whether the body has been consumed. |
content_decoded |
Whether the native streaming path decoded the body. |
response = session.get("https://example.com/account")
print(response.status_code, response.elapsed)
print(response.request.method, response.request.path_url)
print(response.cookies.get_dict())
print(response.http_version, response.connection_reused)
Cancellation and connection control
Session, AsyncSession, and both pool types expose:
cancel()to cancel active native work.close_origin(url)to remove and close the cached route for one origin.close()to permanently close the client.
AsyncSession and AsyncSessionPool also provide acancel() and
aclose_origin(); their close() methods are awaited. Response provides
aclose() for asynchronous stream cleanup. Cancelling one queued pool lease or
one HTTP/2 stream does not cancel unrelated callers.
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}")
print(exc.protocol, exc.phase, exc.code, exc.retryable)
except rex_tls.MobileTLSError as exc:
print(f"Native client error: {exc}")
except (TypeError, ValueError) as exc:
print(f"Invalid configuration: {exc}")
Public exception types:
| Exception | Meaning |
|---|---|
MobileTLSError |
Base native-core error. |
RequestError |
Transport or request execution failed. |
HTTPError |
raise_for_status() received a 4xx or 5xx response. |
InvalidURL |
URL is malformed or unsupported. |
InvalidHeader |
Header cannot be represented safely on the selected wire protocol. |
CookieConflictError |
Cookie lookup matched multiple domain/path identities. |
ContentDecodingError |
Compressed response is malformed, truncated, or over its limit. |
InvalidJSONError |
Base response JSON parsing error. |
JSONDecodeError |
Requests-compatible JSON parsing error with response context. |
SessionClosedError |
Operation used a permanently closed client. |
StreamClosedError |
Body stream was read after close. |
StreamConsumedError |
One-shot body stream was consumed more than once. |
UnrewindableBodyError |
Redirect required replaying a one-shot upload. |
InvalidJSONError and JSONDecodeError both describe invalid response JSON and retain response/request context.
Every RequestError exposes bounded diagnostic fields:
| Field | Meaning |
|---|---|
protocol |
HTTP/1.1, HTTP/2, HTTP/3, TLS, SOCKS5, or None. |
phase |
Stable processing stage such as handshake, settings, flow control, response framing, proxy authentication, or request. |
code |
Stable error category intended for application logging and policy. |
retryable |
True only when the transport can prove the request was not processed. |
rex-tls does not automatically replay a request body merely because
retryable is true. The application remains responsible for deciding whether
its operation is safe to retry.
Requests compatibility
Supported Requests-style behavior includes:
- Top-level request and method helpers.
- Persistent Session state and generic Session.request().
- Query parameters, mappings, ordered duplicate headers, form data, JSON, multipart files, request cookies, and streamed bodies.
- Mutable Session.headers, Session.params, Session.proxies, Session.cookies, and Session.max_redirects.
- Numeric and split timeouts, redirects, certificate verification, HTTP and SOCKS5 proxies, and environment proxy discovery.
- Buffered and streaming Response content, text, JSON, headers, cookies, elapsed time, history, prepared request metadata, links, and status helpers.
The following Requests extension points are not implemented:
- Authentication handler objects passed through
auth=. - Response hooks.
- Transport adapters, adapter mounting, and custom adapter routing.
- Custom RequestsCookieJar policy objects.
- PreparedRequest mutation followed by Session.send().
Applications that depend on one of these extension points must keep Requests or adapt that integration before switching clients.
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 be limited to controlled test endpoints.
Performance
Practical summary
Reuse one Session for repeated calls. It avoids repeated Python setup, reuses TCP/TLS connections, retains TLS session tickets, and allows H2 or H3 multiplexing. AsyncSession is the normal choice for asyncio. Use a pool when the application needs independent state or several H1 connections; adding a pool does not automatically improve an already multiplexed H2 connection.
For eligible strict H2 calls, AsyncSession uses one completion dispatcher instead of one Python worker per in-flight request. In the 2.17.0 release gate, the Python thread peak stayed at four from concurrency 1 through 128 on both Windows and Linux, with one H2 connection. Excluded request shapes retain the previous executor implementation for compatibility.
Controlled localhost measurements show that no client is fastest in every scenario:
- rex-tls has low warm-request overhead and scales strongly in the measured H1 concurrency cases.
- never_primp was slightly faster than rex-tls for the measured single H1 request and remained competitive under concurrency.
- curl_cffi was faster in the measured medium-concurrency H2 case, while the rex-tls pool was faster at the measured high-concurrency H2 point.
- httpcloak was slower than rex-tls in the measured H1 points.
- H2 and H3 stability, connection count, memory use, and protocol behavior should be considered together with raw throughput.
Same-host comparison
The H1 table used a warm client, a 4 KiB response, 100 samples, and the same Windows 11 / CPython 3.14 host. RPS means completed requests per second; higher is better.
| Client | Concurrency 1 | Concurrency 16 |
|---|---|---|
| rex-tls Session | 9,833 RPS | 6,385 RPS |
| rex-tls SessionPool | 9,434 RPS | 8,037 RPS |
| curl_cffi | 4,829 RPS | 2,706 RPS |
| requests | 1,957 RPS | 1,676 RPS |
| never_primp | 10,166 RPS | 7,567 RPS |
| httpcloak | 3,335 RPS | 3,086 RPS |
The controlled H2 table used a 1 ms server delay and 100 samples:
| Client | Concurrency 1 | Concurrency 16 | Concurrency 64 |
|---|---|---|---|
| rex-tls SessionPool | 64 RPS | 930 RPS | 3,012 RPS |
| curl_cffi | 64 RPS | 3,444 RPS | 1,241 RPS |
These are implementation-overhead tests, not a promise for public websites. DNS, TLS handshakes, proxies, server latency, security software, response size, and network quality can change the ranking. Compare clients on the actual workload before choosing one solely for speed.
A 256 MiB streaming upload and download remained incremental in the local regression suite rather than buffering the whole body in Python.
Benchmark definitions, limitations, and reproducible commands are in the performance guide.
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.17.0-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: rex_tls-2.17.0-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 2.6 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 |
0078b58ea0898b7c2716285997a88fbb2c615550c3ae0d24b1d3e0705cba4ef4
|
|
| MD5 |
04f8d85798490043bcada86e5bb2b620
|
|
| BLAKE2b-256 |
935e3aeee24e7290ba070fece8413c68f6d3e8b9881d06edcdfeca169dd3f1c0
|
File details
Details for the file rex_tls-2.17.0-cp39-abi3-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: rex_tls-2.17.0-cp39-abi3-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 2.9 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 |
32206c6751104faf72e932a4a9d5421f0f760eb4aebfe34c96dbe1cbb46007b0
|
|
| MD5 |
bc633b8740309f2e66c29623f9d405c4
|
|
| BLAKE2b-256 |
5c0e0c5fef9f56eaa834802a0995e06335a0e4a932aceb6d709c89b682739464
|