Go
Go's the native API. Every other binding (Python, Node, .NET) calls into the cgo wrapper that wraps this exact code, so the Go surface is the most direct way in and usually the fastest. If your project's already in Go, just use this. No FFI hops, no JSON marshalling between two languages, no native binary to ship.
Install
go get github.com/sardanioss/httpcloak
Module path is github.com/sardanioss/httpcloak. Public package name is httpcloak.
Quick start
package main
import (
"context"
"fmt"
"time"
"github.com/sardanioss/httpcloak"
)
func main() {
s := httpcloak.NewSession("chrome-latest",
httpcloak.WithSessionTimeout(20*time.Second),
)
defer s.Close()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
resp, err := s.Get(ctx, "https://tls.peet.ws/api/all")
if err != nil {
panic(err)
}
defer resp.Close()
body, _ := resp.Text()
fmt.Println("status:", resp.StatusCode)
fmt.Println("proto:", resp.Protocol)
fmt.Println("len:", len(body))
}
Three things worth flagging:
NewSessiontakes a preset name string and a variadic list ofSessionOptionvalues. Full preset catalog at Presets.ctx context.Contextis always the first arg. Idiomatic Go, and you get cancellation and deadlines for free.defer s.Close()anddefer resp.Close(). The session owns connections and a cookie jar. The response body is anio.ReadCloseryou must drain or close.
Two API levels
httpcloak ships two API levels. Most folks want the session level.
Session (recommended)
Persistent. Holds cookies, TLS resumption tickets, ECH configs, the connection pool. Reach for this when you're hitting the same host more than once, when you care about cookies, or when you want browser-style refresh / warmup behaviour.
s := httpcloak.NewSession("chrome-latest")
defer s.Close()
Client (lower level)
Stateless wrapper for one-off requests. No cookie jar. Each Do() builds a fresh request through the same transport stack. Use this when you genuinely don't want state between requests.
c := httpcloak.New("chrome-latest", httpcloak.WithTimeout(15*time.Second))
defer c.Close()
resp, err := c.Get(ctx, "https://example.com")
The two surfaces look similar on purpose, but the option types are different: Option for Client, SessionOption for Session. Don't mix them.
Session methods
Full method list on *httpcloak.Session. Read top to bottom.
Construction
func NewSession(preset string, opts ...SessionOption) *Session
func LoadSession(path string) (*Session, error)
func UnmarshalSession(data []byte) (*Session, error)
NewSession is the normal entry point. LoadSession and UnmarshalSession rebuild a session from a file or JSON blob you saved earlier, see Session save & restore.
Core request
func (s *Session) Do(ctx context.Context, req *Request) (*Response, error)
func (s *Session) DoWithBody(ctx context.Context, req *Request, bodyReader io.Reader) (*Response, error)
func (s *Session) Get(ctx context.Context, url string) (*Response, error)
The Go session API is sparser than Python's or Node's, on purpose. Get is the only one-call shortcut. For POST, PUT, PATCH, DELETE, HEAD, OPTIONS, build a Request struct and call Do:
req := &httpcloak.Request{
Method: "POST",
URL: "https://httpbin.org/post",
Headers: map[string][]string{"Content-Type": {"application/json"}},
Body: bytes.NewReader([]byte(`{"hello":"world"}`)),
}
resp, err := s.Do(ctx, req)
DoWithBody takes the body separately as an io.Reader. Reach for it when you're streaming an upload and don't want to buffer the body upfront.
Streaming responses
func (s *Session) DoStream(ctx context.Context, req *Request) (*StreamResponse, error)
func (s *Session) GetStream(ctx context.Context, url string) (*StreamResponse, error)
func (s *Session) GetStreamWithHeaders(ctx context.Context, url string, headers map[string][]string) (*StreamResponse, error)
StreamResponse exposes Read(p []byte) (int, error), ReadChunk(size int) ([]byte, error), ReadAll() ([]byte, error), and Close() error. Use DoStream for downloads where buffering the whole body in memory is a bad idea: videos, big JSON dumps, archives.
Streaming won't auto-follow redirects. If you get a 3xx back, you handle it manually.
Lifecycle
func (s *Session) Close()
func (s *Session) Refresh()
func (s *Session) RefreshWithProtocol(protocol string) error
func (s *Session) Warmup(ctx context.Context, url string) error
func (s *Session) Fork(n int) []*Session
Close()releases the connection pool and the cookie jar. Always defer it.Refresh()drops connections but keeps cookies and TLS tickets. Like a browser hitting F5.RefreshWithProtocol("h1" | "h2" | "h3" | "auto")does a refresh and switches the wire protocol for next requests. Handy for warming TLS on H3 then serving H2 with resumption.Warmup(ctx, url)does a real-browser-style page load: HTML first, then subresources with proper priorities, headers, and timing. Pop this before hitting an antibot endpoint.Fork(n)makesnchild sessions that share cookies and TLS resumption with the parent but get their own connections. Same browser, multiple tabs.
Persistence
func (s *Session) Save(path string) error
func (s *Session) Marshal() ([]byte, error)
Save writes a JSON blob (cookies, TLS session tickets, ECH configs) to disk. Marshal returns the same blob as bytes if you'd rather stuff it into Redis or a database. Round-trip with LoadSession / UnmarshalSession.
Cookie management
func (s *Session) GetCookies() []CookieInfo
func (s *Session) GetCookiesDetailed() []CookieInfo
func (s *Session) SetCookie(cookie CookieInfo)
func (s *Session) DeleteCookie(name, domain string)
func (s *Session) ClearCookies()
CookieInfo is a type alias for session.CookieState and carries the full name, value, domain, path, expires, maxAge, secure, httpOnly, sameSite set.
Proxy management
func (s *Session) SetProxy(proxyURL string)
func (s *Session) SetTCPProxy(proxyURL string)
func (s *Session) SetUDPProxy(proxyURL string)
func (s *Session) GetProxy() string
func (s *Session) GetTCPProxy() string
func (s *Session) GetUDPProxy() string
SetProxy("") flips the session back to direct. The split TCP/UDP proxy methods exist for when you want H1/H2 over an HTTP proxy and H3 over MASQUE. See Proxies overview.
Header order
func (s *Session) SetHeaderOrder(order []string)
func (s *Session) GetHeaderOrder() []string
Override the preset's header order. Pass lowercase names. Empty slice resets to the preset default.
Other
func (s *Session) SetSessionIdentifier(sessionId string)
Tags this session for distributed TLS cache key isolation when used behind a LocalProxy. Most users won't touch this.
Client methods
*httpcloak.Client for one-off requests:
func New(preset string, opts ...Option) *Client
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
func (c *Client) Get(ctx context.Context, url string) (*Response, error)
func (c *Client) GetWithHeaders(ctx context.Context, url string, headers map[string][]string) (*Response, error)
func (c *Client) Post(ctx context.Context, url string, body io.Reader, contentType string) (*Response, error)
func (c *Client) PostJSON(ctx context.Context, url string, body []byte) (*Response, error)
func (c *Client) PostForm(ctx context.Context, url string, body []byte) (*Response, error)
func (c *Client) PostMultipart(ctx context.Context, url string, fields []MultipartField) (*Response, error)
func (c *Client) Close()
Client only takes WithTimeout and WithProxy. Need more knobs? Switch to Session.
Request struct
type Request struct {
Method string
URL string
Headers map[string][]string // matches http.Header
Body io.Reader
Timeout time.Duration
TLSOnly *bool // per-request override
}
Headers is map[string][]string to match the stdlib http.Header shape and let you send the same header more than once. Lowercase the keys so they line up with the preset's order.
TLSOnly set to &true skips the preset's HTTP headers for this single request (TLS fingerprint still applies). nil means use the session's setting.
Response struct
type Response struct {
StatusCode int
Headers map[string][]string
Body io.ReadCloser
FinalURL string
Protocol string // "http/1.1", "h2", "h3"
History []*RedirectInfo
}
Methods:
func (r *Response) Close() error
func (r *Response) Bytes() ([]byte, error)
func (r *Response) Text() (string, error)
func (r *Response) JSON(v interface{}) error
func (r *Response) GetHeader(key string) string
func (r *Response) GetHeaders(key string) []string
Bytes, Text, JSON cache the body after the first read. GetHeader / GetHeaders look up case-insensitively against the lowercase keys.
History is the list of intermediate redirects you went through. Each entry has StatusCode, URL, and Headers.
Idiomatic patterns
Always defer Close
s := httpcloak.NewSession("chrome-latest")
defer s.Close()
// ...
resp, err := s.Get(ctx, url)
if err != nil { return err }
defer resp.Close()
The session owns network resources. The response body is an io.ReadCloser you have to close.
Pass context
Always thread a context.Context through. That's how cancellation, deadlines, and request-scoped values move around in Go. httpcloak honours ctx.Done() everywhere: at dial, at TLS handshake, at body reads.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := s.Get(ctx, url)
Decode JSON straight into a struct
var payload struct {
UserAgent string `json:"user_agent"`
IP string `json:"ip"`
}
if err := resp.JSON(&payload); err != nil {
return err
}
Response.JSON reads the body once, caches the bytes, then runs encoding/json on them.
Errors
httpcloak returns plain error values. Use errors.Is against context.DeadlineExceeded or context.Canceled to tell timeout errors apart from anything else.
Concurrency
The session's safe for concurrent use. It holds a sync.RWMutex around mutable state (cookies, header order, proxy switches), and the underlying transport pool handles concurrent dials.
In practice:
- One
*Session, many goroutines making requests at once. Fine. - One
*Session, one goroutine callingSetProxy()while another callsGet(). Also fine, the lock orders them. - Reading the same
Response.Bodyfrom multiple goroutines at once. Don't. Each response is single-reader.
If you want true parallelism with shared cookie state, use Fork(n) to get sibling sessions. Each fork has its own connection pool but inherits the parent's cookies and TLS tickets. That's the closest you'll get to "browser tabs" behaviour.
Custom fingerprints
s := httpcloak.NewSession("chrome-latest",
httpcloak.WithCustomFingerprint(httpcloak.CustomFingerprint{
JA3: "771,4865-4866-4867-...,0-23-65281-...,29-23-24,0",
Akamai: "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
}),
)
Setting JA3 automatically flips the session into TLS-only mode (HTTP headers from the preset get skipped). See Custom JA3 and Akamai shorthand.
What about HTTP/3?
Same Session.Get, Session.Do, etc. The transport picks the protocol via Alt-Svc, ALPN, and a small race between H3 and H2 dials. Force a specific one with WithForceHTTP1, WithForceHTTP2, WithForceHTTP3. The Response.Protocol field tells you what actually went on the wire.
See also
- Options reference: every
SessionOptionflag with a one-line description. - Connection lifecycle: refresh, warmup, fork, save, load.
- Cookies and state: jar internals and per-request overrides.
- Proxies: HTTP, SOCKS5, MASQUE, source-IP binding.