Every Byte of your Request Indistinguishable from Chrome.
📖 Full documentation at httpcloak.dev
The Problem
Bot detection doesn't just check your User-Agent anymore.
It fingerprints your TLS handshake. Your HTTP/2 frames. Your QUIC parameters. The order of your headers. Whether your SNI is encrypted.
One mismatch = blocked.
The Solution
import httpcloak
r = httpcloak.get("https://target.com", preset="chrome-latest")
That's it. Full browser transport layer fingerprint.
What Gets Emulated
🔐 TLS Layer
|
🚀 Transport Layer
|
🧠 Header Layer
|
Results
┌─────────────────────────────────┐
│ ECH (Encrypted Client Hello) │
├─────────────────────────────────┤
│ WITHOUT: sni=plaintext │
│ WITH: sni=encrypted + │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ HTTP/3 Fingerprint Match │
├─────────────────────────────────┤
│ Protocol: h3 + │
│ QUIC Version: 1 + │
│ Transport Params: + │
│ GREASE Frames: + │
└─────────────────────────────────┘
Install
pip install httpcloak # Python
npm install httpcloak # Node.js
go get github.com/sardanioss/httpcloak # Go
dotnet add package HttpCloak # C#
Quick Start
Python
import httpcloak
# Simple request
r = httpcloak.get("https://example.com", preset="chrome-latest")
print(r.status_code, r.protocol)
# POST with JSON
r = httpcloak.post("https://httpbin.org/post",
json={"key": "value"},
preset="chrome-latest"
)
# Custom headers
r = httpcloak.get("https://httpbin.org/headers",
headers={"X-Custom": "value"},
preset="chrome-latest"
)
Go
import (
"context"
"github.com/sardanioss/httpcloak/client"
)
// Simple request
c := client.NewClient("chrome-latest")
defer c.Close()
resp, _ := c.Get(ctx, "https://example.com", nil)
body, _ := resp.Text()
fmt.Println(resp.StatusCode, resp.Protocol)
// POST with JSON
jsonBody := []byte(`{"key": "value"}`)
resp, _ = c.Post(ctx, "https://httpbin.org/post",
bytes.NewReader(jsonBody),
map[string][]string{"Content-Type": {"application/json"}},
)
// Custom headers
resp, _ = c.Get(ctx, "https://httpbin.org/headers", map[string][]string{
"X-Custom": {"value"},
})
Node.js
import httpcloak from "httpcloak";
// Simple request
const session = new httpcloak.Session({ preset: "chrome-latest" });
const r1 = await session.get("https://example.com");
console.log(r1.statusCode, r1.protocol);
// POST with JSON
const r2 = await session.post("https://httpbin.org/post", {
json: { key: "value" }
});
// Custom headers
const r3 = await session.get("https://httpbin.org/headers", {
headers: { "X-Custom": "value" }
});
session.close();
C#
using HttpCloak;
// Simple request
using var session = new Session(preset: Presets.Chrome145);
var r1 = session.Get("https://example.com");
Console.WriteLine($"{r1.StatusCode} {r1.Protocol}");
// POST with JSON
var r2 = session.PostJson("https://httpbin.org/post",
new { key = "value" }
);
// Custom headers
var r3 = session.Get("https://httpbin.org/headers",
headers: new Dictionary<string, string> { ["X-Custom"] = "value" }
);
Features
🧬 Build Any Browser Fingerprint From JSON
Don't have a preset for your target browser? Capture once, use forever. Visit tls.peet.ws/api/all in the browser you want to mimic, paste the JA3 + Akamai fingerprint into a JSON spec, register it, and you have a brand-new preset that emits real wire bytes.
import json, httpcloak
# 1. Capture: visit tls.peet.ws/api/all in the browser, copy two fields.
PEET_JA3 = "771,4865-4866-4867-49195-49199-49196-49200-...,29-23-24,0"
PEET_AKAMAI = "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"
# 2. Start from any built-in preset, swap in the captured fingerprint.
spec = json.loads(httpcloak.describe_preset("chrome-latest"))
spec["preset"]["name"] = "my-browser"
spec["preset"]["tls"] = {"ja3": PEET_JA3}
spec["preset"]["http2"]["akamai"] = PEET_AKAMAI
# 3. Register, use like any built-in preset.
httpcloak.load_preset_from_json(json.dumps(spec))
session = httpcloak.Session(preset="my-browser")
r = session.get("https://target.com/")
describe_preset emits every effective field — TLS extensions, HTTP/2 SETTINGS order, HPACK encoding order, per-resource-type stream priority table, QUIC transport params, TCP/IP fingerprint, full header set — so anything you see in the JSON is editable. Mutated specs round-trip byte-equal through load_preset_from_json → run → describe_preset: same wire mechanics, just the values you changed.
Same workflow across all bindings:
| Describe | Load | Unregister | |
|---|---|---|---|
| Python | httpcloak.describe_preset(name) |
httpcloak.load_preset_from_json(json) |
httpcloak.unregister_preset(name) |
| Node.js | describePreset(name) |
loadPresetFromJSON(json) |
unregisterPreset(name) |
| .NET | CustomPresets.Describe(name) |
CustomPresets.LoadFromJson(json) |
CustomPresets.Unregister(name) |
| Go | fingerprint.Describe(name) |
fingerprint.LoadPresetFromJSON(json) |
fingerprint.Unregister(name) |
Pool dozens of fingerprints with PresetPool (round-robin / random rotation, all bindings). Drill-down recipes — bumping a single H2 priority, inserting an HPACK header, importing a peet.ws capture, cleaning up — in examples/python-examples/17_tweak_fingerprint.py, examples/js-examples/18_tweak_fingerprint.js, and examples/csharp-examples/TweakFingerprint.cs.
🔐 ECH (Encrypted Client Hello)
Hides which domain you're connecting to from network observers.
session = httpcloak.Session(
preset="chrome-latest",
ech_config_domain="cloudflare-ech.com" # Fetches ECH config from DNS
)
Cloudflare trace shows sni=encrypted instead of sni=plaintext. Use cloudflare-ech.com (the dedicated ECH domain) for any Cloudflare-fronted target.
⚡ Session Resumption (0-RTT)
TLS session tickets make you look like a returning visitor.
# Warm up on any Cloudflare site
session.get("https://cloudflare.com/")
session.save("session.json")
# Use on your target
session = httpcloak.Session.load("session.json")
r = session.get("https://target.com/") # Bot score: 99
Cross-domain warming works because Cloudflare sites share TLS infrastructure.
🌐 HTTP/3 Through Proxies
Two methods for QUIC through proxies:
| Method | How it works |
|---|---|
| SOCKS5 UDP ASSOCIATE | Proxy relays UDP packets. Most residential proxies support this. |
| MASQUE (CONNECT-UDP) | RFC 9298. Tunnels UDP over HTTP/3. Premium providers only. |
# SOCKS5 with UDP
session = httpcloak.Session(proxy="socks5://user:pass@proxy:1080")
# MASQUE
session = httpcloak.Session(proxy="masque://proxy:443")
Known MASQUE providers (auto-detected): Bright Data, Oxylabs, Smartproxy, SOAX.
Speculative TLS (opt-in): CONNECT + TLS ClientHello are sent together, saving one proxy round-trip (~25% faster). Enable for compatible proxies:
session = httpcloak.Session(proxy="socks5://...", enable_speculative_tls=True)
🎭 Domain Fronting
Connect to a different host than what appears in TLS SNI.
session := httpcloak.NewSession("chrome-latest",
httpcloak.WithConnectTo("public-cdn.com", "actual-backend.internal"),
)
defer session.Close()