tls-client gives Go a net/http shape and a browser-shaped TLS handshake
net/http.Client like HTTP Client with options to select specific client TLS Fingerprints to use for requests.
At a glance
- What is it?
- tls-client is a BSD-4-Clause Go library whose HttpClient interface mirrors net/http so that most existing code keeps compiling, while the handshake underneath is driven by a browser profile instead of Go's standard TLS stack. It also forks the transport, the TLS library and the QUIC library, which is the decision that defines its upgrade cost.
- Who is it for?
- Adopt tls-client if you maintain Go scrapers or integrations that already speak net/http and hit servers that classify clients by handshake, because the interface is a superset of the standard one and existing request code mostly survives the swap. Do not adopt it as a general HTTP client for services you own or have permission to test: it exists to look like someone else's browser, which is not what you want in a client for your own API.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- bogdanfinn/tls-client1,934 ★ · +39
| Repository | 2026-10-17 | 2026-11-07 |
|---|---|---|
| bogdanfinn/tls-client | 1.9k | 1.9k |
39 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The interface is a net/http superset, so most request code survives
The core design decision is that the client type is shaped like the one already in your codebase. The `HttpClient` interface extends the base net/http client with cookie, proxy and TLS accessors:
type HttpClient interface {
GetCookies(u *url.URL) []*http.Cookie
SetCookies(u *url.URL, cookies []*http.Cookie)
SetCookieJar(jar http.CookieJar)
GetCookieJar() http.CookieJar
SetProxy(proxyUrl string) error
GetProxy() string
SetFollowRedirect(followRedirect bool)
GetFollowRedirect() bool
CloseIdleConnections()
Do(req *http.Request) (*http.Response, error)
Get(url string) (resp *http.Response, err error)
Head(url string) (resp *http.Response, err error)
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
GetBandwidthTracker() bandwidth.BandwidthTracker
GetDialer() proxy.ContextDialer
GetTLSDialer() TLSDialerFunc
}`Do`, `Get`, `Head` and `Post` take the same arguments and return the same `*http.Response` as the standard library, so migrating a caller is mostly a change of import. The extras are on top: cookie jar manipulation, proxy configuration, a bandwidth tracker, and direct access to the dialer and the TLS dialer function if you need to reach past the abstraction.
The subtle cost of that compatibility is the import alias. Because the transport is a fork, the README's own example imports the HTTP package separately from the client:
import (
"fmt"
"io"
"log"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
)Note that `http` here is `github.com/bogdanfinn/fhttp`, not `net/http`. The interface signature and the concrete types are compatible in shape but not in identity, so code that passes a `*net/http.Request` across the boundary does not compile without conversion. It is a superset in API design, not in type identity.
A profile is a named browser version, and a stale name is a wrong fingerprint
The point of the library is that a server can classify a client by the shape of its TLS handshake, not just by the user-agent string. The README states plainly that changing a user-agent is no longer sufficient, because the server may use TLS fingerprinting to detect the client browser.
The way you select a fingerprint is by profile, and the profile names are browser versions:
options := []tls_client.HttpClientOption{
tls_client.WithTimeoutSeconds(30),
tls_client.WithClientProfile(profiles.Chrome_150),
tls_client.WithNotFollowRedirects(),
tls_client.WithCookieJar(jar), // create cookieJar instance and pass it as argument
}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)That naming is the operational reality of this kind of library. `profiles.Chrome_150` claims to be Chrome 150, and it will keep claiming that until someone updates it. A profile that lags the real browser is worse than no profile, because it produces a consistent, confident but stale signature. The repository carries `profiles/` at top level with a `mapper.go` alongside it, which is where that translation lives.
Client behaviour is configured through functional options on a slice, which is a pleasant pattern in Go and keeps construction order explicit. `WithNotFollowRedirects` in the example is worth noting: redirect handling is opt-out rather than opt-in, so a client that does not ask for redirects will not follow them.
Header order is part of the signal, not a formatting detail
The README's example does something unusual that is easy to miss when skimming. The request header is assigned wholesale and then an ordering key is injected:
req.Header = http.Header{
"accept": {"*/*"},
"accept-language": {"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"},
"user-agent": {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"},
http.HeaderOrderKey: {
"accept",
"accept-language",
"user-agent",
},
}`http.HeaderOrderKey` is a reserved map key that carries the sequence in which headers should be sent. Go's `net/http` treats a header map as unordered, so the wire order normally falls out of map iteration. Browsers send headers in a fixed order, and a server comparing your request against a browser request will notice a discrepancy even when every value matches. Custom header ordering is listed as a feature for exactly this reason.
The corollary is that claiming to be Chrome is a three-part commitment: the TLS handshake profile, the header values, and the header order. The library supplies the profile and the ordering mechanism; the values and the language preferences in that example, `de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7`, are yours to get right, and getting them wrong partially defeats the point.
Four forked dependencies and what that costs you
The go.mod is where the real engineering picture appears. The module is `github.com/bogdanfinn/tls-client` and it requires Go 1.24.1, which is a recent toolchain floor and will exclude some older CI images.
The go.mod lists four direct dependencies under this project's own namespace:
github.com/bogdanfinn/fhttp v0.6.9
github.com/bogdanfinn/quic-go-utls v1.0.10-utls
github.com/bogdanfinn/utls v1.7.8-barnius
github.com/bogdanfinn/websocket v1.5.6-barniusThe README states the reason plainly: the library is built upon Carcraftz's fhttp and utls, and that the original repositories from Carcraftz appear not to be maintained anymore. The suffix `-barnius` on two of them points at the author. This is not a criticism; it is the ordinary shape of a project that must patch a TLS stack to add something upstream does not have. It does mean that when a CVE lands in upstream utls or quic-go, it lands in a fork, and your patch window is however fast this author merges rather than however fast the upstream project ships.
The remaining direct dependencies are ordinary: `bdandy/go-socks4` for SOCKS support, `google/uuid`, `golang.org/x/net`, `stretchr/testify` for tests, and `tam7t/hpkp` for HTTP public key pinning. Indirect entries include brotli and klauspost/compress for decompression, circl and qpack for QUIC, and the usual golang.org/x modules. The commented-out `replace` directives for utls, quic-go-utls, websocket and fhttp at the bottom of go.mod are there for local development against checkouts, not for consumption.
Licence is BSD-4-Clause, which is permissive and carries the advertising clause variant.
HTTP/3 with protocol racing, and what it changes about failure
Protocol support covers HTTP/1.1, HTTP/2 and HTTP/3 with automatic negotiation, and the README lists HTTP/3 fingerprinting separately, meaning the QUIC layer also carries a browser-shaped signature. Protocol racing is described as Chrome-like Happy Eyeballs for HTTP/2 versus HTTP/3, and there is a `racer.go` at the top level plus an `example/protocol_racing/` directory.
This is where a library that mimics a browser starts to behave unlike a well-behaved client. The README describes racing as Chrome-like Happy Eyeballs between HTTP/2 and HTTP/3. From the server's perspective the losing connection is still a connection. If your traffic is unexpected or high volume, that is a behaviour worth understanding before you turn it on rather than after.
Beyond the handshake, the file layout shows a broad surface: `websocket.go` and `websocket_options.go` for WebSocket connections that keep the fingerprint, `pinner.go` for certificate pinning, `jar.go` for cookies, `bandwidth/` for tracking upload and download volume, `ja3.go` and `ja3_trust_anchors_test.go` for JA3 fingerprint handling, and `socks5_udp.go` with its test for SOCKS UDP. The `example/` directory is the best documentation in the repository: separate directories for concurrency, connection reuse, client per worker, custom profiles, custom specs, decompression, download, hooks, post, proxy, redirects and session tickets.
Language bindings beyond Go are offered through FFI, with the README listing JavaScript on Node.js, Python and C#. The `cffi_src/` and `cffi_dist/` directories at top level are the mechanism for the C# and Python side.
Where this is the wrong tool
The library's stated purpose is making a Go program indistinguishable from a browser to a server. If you are writing a client for an API you own, or for a service you have permission to test, this is the wrong shape. The added profile machinery may not be useful for an ordinary API client.
There is also a durability question specific to this design. Fingerprint accuracy is a moving target tied to browser release cadence, so the profiles need updating when Chrome ships. Whether that happens promptly is not something the README commits to. The last push was on 2026-09-04 and the most recent release is v1.16.0 from 2026-09-02, following v1.15.1 on 2026-06-08 and v1.15.0 on 2026-06-05, so releases land close to commits.
And note what the README says about its own limits. It carries a sponsor block stating that TLS fingerprinting alone is not enough for modern bot protection and pointing at a commercial service that returns antibot tokens for Akamai, DataDome, Kasada and Incapsula. That is an admission from the maintainer that the handshake layer has a ceiling, and it is the honest signal that this library is one layer of a problem, not a solution to it.
Editorial conclusion
Adopt tls-client if you maintain Go scrapers or integrations that already speak net/http and hit servers that classify clients by handshake, because the interface is a superset of the standard one and existing request code mostly survives the swap. Do not adopt it as a general HTTP client for services you own or have permission to test: it exists to look like someone else's browser, which is not what you want in a client for your own API. Before upgrading, review release and dependency changes, since go.mod pins four dependencies under this project's namespace and a fingerprint profile is only accurate while it matches the browser version it names, such as Chrome_150.
Frequently asked questions
What Go version does tls-client require?
The go.mod declares `go 1.24.1`. That is a recent toolchain floor, so older CI images will need upgrading before the module will build.
How do I set a specific browser TLS fingerprint in tls-client?
Pass a profile from the `profiles` package to the client option `WithClientProfile`, for example `tls_client.WithClientProfile(profiles.Chrome_150)`. Profile names carry a browser version, so a profile only stays accurate while it matches that browser release.
Does tls-client use the standard library net/http transport?
No. It is built on a fork, `github.com/bogdanfinn/fhttp`, along with `github.com/bogdanfinn/utls`, `quic-go-utls` and `websocket`, because the README states the original upstream repositories are not maintained. The `HttpClient` interface mirrors net/http, but the concrete request types come from the fork.
What is protocol racing in tls-client?
It is described as a Chrome-like Happy Eyeballs for HTTP/2 versus HTTP/3, opening both and keeping whichever responds first. A `racer.go` file and an `example/protocol_racing/` directory exist in the repository.
Is tls-client enough to get past modern bot protection?
No, by the maintainer's own account. The README states TLS fingerprinting alone is not enough for modern bot protection and that additional antibot tokens are needed for systems such as Akamai, DataDome, Kasada and Incapsula.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bogdanfinn-tls-client)Star history chart for your README
[](https://hysenlabs.com/projects/bogdanfinn-tls-client)Was this useful?