Advanced Usage¶
Async Patterns¶
Concurrent Requests with asyncio.gather¶
The biggest advantage of async is running many requests in parallel:
import asyncio
import cycletls
async def main():
# 10 requests execute concurrently
responses = await asyncio.gather(*[
cycletls.aget(f"https://httpbin.org/get?id={i}")
for i in range(10)
])
for r in responses:
print(r.status_code)
asyncio.run(main())
Rate Limiting with Semaphore¶
Control how many requests run at once:
import asyncio
import cycletls
async def main():
semaphore = asyncio.Semaphore(5) # max 5 concurrent
async def limited(url):
async with semaphore:
return await cycletls.aget(url)
responses = await asyncio.gather(*[
limited(f"https://httpbin.org/get?id={i}")
for i in range(100)
])
asyncio.run(main())
Error Handling in Async¶
import asyncio
import cycletls
from cycletls import CycleTLSError
async def main():
results = await asyncio.gather(
cycletls.aget("https://httpbin.org/status/200"),
cycletls.aget("https://httpbin.org/status/404"),
return_exceptions=True,
)
for result in results:
if isinstance(result, Exception):
print(f"Error: {result}")
else:
print(f"OK: {result.status_code}")
asyncio.run(main())
Custom TLS Fingerprints¶
JA3 Fingerprinting¶
JA3 captures the TLS Client Hello parameters as a single string. Format:
TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
Example with a Chrome 83 JA3:
import cycletls
response = cycletls.get(
"https://ja3er.com/json",
ja3="771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36",
)
print(response.json()["ja3_hash"])
JA4R Fingerprinting¶
JA4R provides more granular control than JA3. Use ja4r= (raw format) to
configure fingerprints. JA4 hashes are observation-only and cannot be used as
input.
from cycletls import CycleTLS
with CycleTLS() as client:
response = client.get(
"https://tlsfingerprint.com/api/all",
ja4r="t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
disable_grease=False,
user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
)
data = response.json()
print(f"JA4: {data['tls']['ja4']}")
print(f"JA4_r: {data['tls']['ja4_r']}")
JA4R format breakdown:
t13d1516h2_<ciphers>_<extensions>_<signature_algorithms>
t= transport (TCP)13= TLS 1.3d= SNI present15= cipher count16= extension counth2= ALPN (HTTP/2)
HTTP/2 Fingerprinting¶
Different browsers send different HTTP/2 SETTINGS frames. Match them:
from cycletls import CycleTLS
with CycleTLS() as client:
# Firefox HTTP/2 fingerprint
response = client.get(
"https://tlsfingerprint.com/api/all",
http2_fingerprint="1:65536;2:0;4:131072;5:16384|12517377|0|m,p,a,s",
ja3="771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0",
user_agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0",
)
HTTP/2 fingerprint format:
<settings>|<window_update>|<priority>|<pseudo_header_order>
Common browser fingerprints:
Browser |
HTTP/2 Fingerprint |
|---|---|
Firefox |
|
Chrome |
|
Creating Custom Profiles¶
Bundle JA3, HTTP/2, and User-Agent into a reusable profile:
from cycletls import TLSFingerprint, FingerprintRegistry
my_profile = TLSFingerprint(
name="custom_chrome",
ja3="771,4865-4866-4867-49195...",
http2_fingerprint="1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
user_agent="Mozilla/5.0 ...",
)
# Register for lookup by name
registry = FingerprintRegistry()
registry.register(my_profile)
# Use anywhere
response = cycletls.get("https://example.com", fingerprint=my_profile)
# or by name:
response = cycletls.get("https://example.com", fingerprint="custom_chrome")
Loading Profiles from Files¶
Store profiles as JSON and load them at startup:
{
"name": "chrome_125",
"ja3": "771,4865-4866-4867-49195...",
"user_agent": "Mozilla/5.0 ...",
"http2_fingerprint": "1:65536;2:0;4:6291456..."
}
from cycletls import load_fingerprint_from_file, load_fingerprints_from_dir
# Single file
profile = load_fingerprint_from_file("profiles/chrome_125.json")
# Entire directory (auto-registers all found profiles)
load_fingerprints_from_dir("profiles/")
Set the CYCLETLS_FINGERPRINT_DIR environment variable to auto-load a
directory of profiles:
from cycletls import load_fingerprints_from_env
load_fingerprints_from_env() # reads CYCLETLS_FINGERPRINT_DIR
Proxy Configuration¶
CycleTLS supports HTTP, HTTPS, SOCKS4, SOCKS5, and SOCKS5h proxies.
Per-Request Proxy¶
import cycletls
response = cycletls.get(
"https://httpbin.org/ip",
proxy="socks5://127.0.0.1:9050",
)
Default Proxy for All Requests¶
import cycletls
cycletls.set_default(proxy="socks5://user:pass@proxy.example.com:1080")
# All requests now go through the proxy
response = cycletls.get("https://httpbin.org/ip")
Supported Proxy Protocols¶
Protocol |
URL Format |
|---|---|
HTTP |
|
HTTPS |
|
SOCKS4 |
|
SOCKS5 |
|
SOCKS5 with DNS |
|
All protocols support user:password@ authentication in the URL.
HTTP/3 and QUIC¶
Force HTTP/3 for servers that support it:
from cycletls import CycleTLS
with CycleTLS() as client:
response = client.get(
"https://cloudflare-quic.com/",
force_http3=True,
insecure_skip_verify=True,
)
print(f"Response over HTTP/3: {response.status_code}")
Error Handling¶
Checking Response Status¶
import cycletls
response = cycletls.get("https://httpbin.org/status/404")
# Boolean checks
if response.is_error:
print(f"Error: {response.status_code}")
if not response.ok:
print(f"Not OK: {response.reason}")
Raising Exceptions¶
from cycletls import CycleTLSError, HTTPError
try:
response = cycletls.get("https://httpbin.org/status/500")
response.raise_for_status()
except HTTPError as e:
print(f"HTTP {e.response.status_code}: {e}")
Catching All CycleTLS Errors¶
Every CycleTLS exception inherits from CycleTLSError:
from cycletls import CycleTLSError
try:
response = cycletls.get("https://invalid-host.example")
except CycleTLSError as e:
print(f"CycleTLS error: {e}")
WebSocket Support¶
CycleTLS includes a WebSocket client with TLS fingerprinting:
from cycletls import WebSocketConnection
ws = WebSocketConnection(
"wss://echo.websocket.org",
ja3="771,4865-4866-4867-49195...",
user_agent="Mozilla/5.0 ...",
)
ws.connect()
ws.send("Hello, WebSocket!")
message = ws.receive()
print(message.data)
ws.close()
Server-Sent Events (SSE)¶
Stream server-sent events:
from cycletls import SSEConnection
sse = SSEConnection("https://example.com/events")
for event in sse.connect():
print(f"Event: {event.event}")
print(f"Data: {event.data}")
print(f"ID: {event.id}")
Logging¶
CycleTLS uses Python’s standard logging module. Enable debug logging to
see request and response details:
import logging
import cycletls
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
response = cycletls.get("https://httpbin.org/get")
Output:
DEBUG:cycletls.api:Sending GET request to https://httpbin.org/get
DEBUG:cycletls.api:Request headers: {...}
DEBUG:cycletls._ffi:Calling Go shared library getRequest()
DEBUG:cycletls._ffi:Received response from Go (size: 1234 bytes)
DEBUG:cycletls.api:Received response: 200 OK
Log levels:
Level |
What is logged |
|---|---|
|
Request/response details, headers, FFI calls, response sizes |
|
Library loading events |
|
Request failures, parsing errors |
Binary Data¶
Downloading Binary Content¶
import cycletls
response = cycletls.get("https://example.com/image.png")
with open("image.png", "wb") as f:
f.write(response.content)
Uploading Files¶
import cycletls
response = cycletls.post(
"https://httpbin.org/post",
files={"file": ("report.pdf", open("report.pdf", "rb"), "application/pdf")},
)