Go HTTP client with browser-identical TLS/HTTP2 fingerprinting. Bypass bot detection by perfectly mimicking Chrome, Firefox, and Safari at the cryptographic lev
Go HTTP client with browser-identical TLS/HTTP2 fingerprinting. Bypass bot detection by perfectly mimicking Chrome, Firefox, and Safari at the cryptographic lev
Every Byte of your Request Indistinguishable from Chrome.
Full documentation at httpcloak.dev
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.
import httpcloak
r = httpcloak.get("https://target.com", preset="chrome-latest")
That's it. Full browser transport layer fingerprint.
┌─────────────────────────────────┐
│ ECH (Encrypted Client Hello) │
├─────────────────────────────────┤
│ WITHOUT: sni=plaintext │
│ WITH: sni=encrypted + │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ HTTP/3 Fingerprint Match │
├─────────────────────────────────┤
│ Protocol: h3 + │
│ QUIC Version: 1 + │
│ Transport Params: + │
│ GREASE Frames: + │
└─────────────────────────────────┘
pip install httpcloak # Python
npm install httpcloak # Node.js
go get github.com/sardanioss/httpcloak # Go
dotnet add package HttpCloak # C#
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"
)
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"},
})
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();
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 { ["X-Custom"] = "value" }
);
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.
…
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.
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.
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.
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)
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()
Pin a key, or take verification over entirely with your own callback:
c := client.NewClient("chrome-latest")
defer c.Close()
// Pin an SPKI SHA-256, scoped to one host
c.PinCertificate("sha256/AAAA...", client.ForHost("api.example.com"), client.IncludeSubdomains())
Or run your own checks, mirroring crypto/tls:
session := httpcloak.NewSession("chrome-latest",
httpcloak.WithVerifyPeerCertificate(func(rawCerts [][]byte, chains [][]*x509.Certificate) error {
// Return an error to abort the handshake.
return myPinCheck(rawCerts)
}),
)
defer session.Close()
WithVerifyConnection gives you the completed tls.ConnectionState. WithTLSConfig
accepts a standard *tls.Config but reads only its verification fields: anything that
would reshape the ClientHello is ignored on purpose, since that would change how you
look on the wire.
client.OnPreRequest(func(req *http.Request) error {
req.Header.Set("X-Custom", "value")
return nil
})
client.OnPostResponse(func(resp *httpcloak.Response) {
log.Printf("Got %d from %s", resp.StatusCode, resp.FinalURL)
})
fmt.Printf("DNS: %dms, TCP: %dms, TLS: %dms, Total: %dms\n",
resp.Timing.DNSLookup,
resp.Timing.TCPConnect,
resp.Timing.TLSHandshake,
resp.Timing.Total)
session = httpcloak.Session(preset="chrome-latest", http_version="h3") # Force HTTP/3
session = httpcloak.Session(preset="chrome-latest", http_version="h2") # Force HTTP/2
session = httpcloak.Session(preset="chrome-latest", http_version="h1") # Force HTTP/1.1
Auto mode tries HTTP/3 first, falls back gracefully.
Anti-bot systems inspect TCP SYN packet parameters (TTL, Window Size, MSS, Window Scale) to verify your claimed OS matches. A request claiming Chrome on Windows but with Linux TCP parameters (TTL=64) is instantly flagged.
httpcloak automatically sets the correct TCP/IP fingerprint for each preset's platform. You can also override manually:
session = httpcloak.Session(
preset="chrome-latest-windows",
tcp_ttl=128, # Windows=128, Linux/macOS=64
tcp_window_size=64240, # Windows=64240, Linux/macOS=65535
tcp_window_scale=8, # Windows=8, Linux=7, macOS=6
tcp_mss=1460, # Standard Ethernet MTU
)
Go:
session := httpcloak.NewSession("chrome-latest-windows",
httpcloak.WithTCPFingerprint(fingerprint.TCPFingerprint{
TTL: 128, MSS: 1460, WindowSize: 64240, WindowScale: 8, DFBit: true,
}),
)
defer session.Close()
Node.js:
const session = new httpcloak.Session({
preset: "chrome-latest-windows",
tcpTtl: 128,
tcpWindowSize: 64240,
tcpWindowScale: 8,
});
C#:
var session = new Session(
preset: Presets.Chrome145Windows,
tcpTtl: 128,
tcpWindowSize: 64240,
tcpWindowScale: 8
);
Built-in platform profiles: Windows (TTL=128, WS=8), Linux (TTL=64, WS=7), macOS (TTL=64, WS=6).
Switch proxies mid-session without creating new connections. Perfect for proxy rotation.
session = httpcloak.Session(preset="chrome-latest")
# Start with direct connection
r = session.get("https://api.ipify.org")
print(f"Direct IP: {r.text}")
# Switch to proxy 1
session.set_proxy("http://proxy1.example.com:8080")
r = session.get("https://api.ipify.org")
print(f"Proxy 1 IP: {r.text}")
# Switch to proxy 2
session.set_proxy("socks5://proxy2.example.com:1080")
r = session.get("https://api.ipify.org")
print(f"Proxy 2 IP: {r.text}")
# Back to direct
session.set_proxy("")
Split proxy configuration - use different proxies for HTTP/2 and HTTP/3:
session = httpcloak.Session(preset="chrome-latest")
# TCP proxy for HTTP/1.1 and HTTP/2
session.set_tcp_proxy("http://tcp-proxy.example.com:8080")
# UDP proxy for HTTP/3 (requires SOCKS5 UDP ASSOCIATE or MASQUE)
session.set_udp_proxy("socks5://udp-proxy.example.com:1080")
# Check current configuration
print(session.get_tcp_proxy()) # TCP proxy URL
print(session.get_udp_proxy()) # UDP proxy URL
A partial list is a prefix, not a replacement. The names you pass are emitted first, in your order; every header you leave out keeps its normal position from the preset's own table, and anything still unplaced follows sorted by name. So naming one header costs you nothing for the rest. Passing a complete list gives you exactly
No open issues yet, or sync has not completed.