← All guides
DEVELOPER GUIDES · 6 MIN READ

tls-client Proxy Setup in Go with TLS Impersonation

Route bogdanfinn/tls-client through an authenticated proxy, validate the exit and target body with one Go client, and avoid the HTTP/3 proxy leak pitfall.

This guide targets the Go module github.com/bogdanfinn/tls-client. The buyer job is a raw HTTP request with a selected egress and a chosen browser TLS profile. The library exposes a net/http-like client, proxy support, cookie handling, and HTTP/1.1, HTTP/2 and HTTP/3 options. It is not a DOM browser and does not execute page JavaScript.

Search results for “tls-client proxy” also surface the Python package commonly imported as tls_client. That is a separate wrapper ecosystem. The examples here use the official Go repository and its current v1.16.0 release. Matching the query is useful; conflating the APIs makes the setup unusable.

Add the module and pin the version in the Go project that owns the request:

go get github.com/bogdanfinn/tls-client@v1.16.0

Pick the proxy protocol deliberately

Job Option Boundary to verify
HTTPS proxy or HTTP CONNECT route WithProxyUrl("http://...") or https://... Proxy handshake and target TLS are separate hops
SOCKS route WithProxyUrl("socks5://...") SOCKS authentication and DNS behavior
Browser TLS profile WithClientProfile(profiles.Chrome_150) TLS and protocol shape, not browser DOM behavior
One logical session Reuse one HttpClient Cookies and connection reuse stay with that client
Independent identities New client or documented proxy change Rotation is owned by the proxy contract

The official options code accepts a full proxy URL and documents credentials in the URL form. Put that URL in a secret environment binding. Avoid logging it, including in an error string or debug trace.

Run an exit check and a useful target check

The following uses the same client for an IP response and a fixed target response. It reads and closes both bodies, disables redirects, applies the documented hard request deadline, and rejects a body that does not contain the expected value:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "strings"

    tls_client "github.com/bogdanfinn/tls-client"
    "github.com/bogdanfinn/tls-client/profiles"
)

func getBody(client tls_client.HttpClient, url string) (int, string, error) {
    response, err := client.Get(url)
    if err != nil {
        return 0, "", err
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        return response.StatusCode, "", err
    }
    return response.StatusCode, string(body), nil
}

func main() {
    proxyURL := os.Getenv("PROXY_URL")
    if proxyURL == "" {
        log.Fatal("PROXY_URL is required")
    }

    client, err := tls_client.NewHttpClient(
        tls_client.NewNoopLogger(),
        tls_client.WithProxyUrl(proxyURL),
        tls_client.WithTimeoutSeconds(30),
        tls_client.WithClientProfile(profiles.Chrome_150),
        tls_client.WithNotFollowRedirects(),
    )
    if err != nil {
        log.Fatal("client setup failed")
    }
    defer client.CloseIdleConnections()

    exitStatus, exitBody, err := getBody(client, "https://api.ipify.org?format=json")
    if err != nil || exitStatus < 200 || exitStatus >= 300 || !strings.Contains(exitBody, `"ip"`) {
        log.Fatal("exit check failed")
    }

    targetStatus, targetBody, err := getBody(client, "https://example.com/")
    if err != nil || targetStatus < 200 || targetStatus >= 300 || !strings.Contains(targetBody, "Example Domain") {
        log.Fatal("target body check failed")
    }
    fmt.Println("exit body valid; target body valid")
}

WithTimeoutSeconds(30) is a hard deadline for the request lifecycle, including redirects and body reads. WithNotFollowRedirects makes the bounded check inspect the response it received. The official quick usage example also closes resp.Body; the helper keeps that cleanup in one place. The program prints only validation status, never the proxy URL or returned body.

The two checks share a client, but the provider may rotate the IP on each request. If the workflow needs continuity, confirm the provider's sticky-session rule and keep the same client and proxy session. Cookies belong to the client jar; a new client starts a different logical sequence. For independent records, rotate only between requests and record the proxy identity with the result metadata you retain privately.

The HTTP/3 proxy trap

The v1.16.0 release notes make a narrow security point: protocol racing with a non-SOCKS5 proxy is rejected because the HTTP/3 leg could otherwise go direct. HTTP/3 uses UDP, so an HTTP proxy cannot carry that leg in the same way. Use a socks5:// route when the workflow needs HTTP/3 through a proxy, or disable protocol racing and HTTP/3 for a non-SOCKS5 route. Verify the observed exit with the same client after changing this setting.

Do not turn a successful HTTP/1.1 or HTTP/2 response into a claim that HTTP/3 is safe for the same proxy. Protocol selection changes the transport path. The release notes also call out a concurrency issue for racing legs; high-volume callers should prefer the documented non-racing path until their own workload proves otherwise.

Diagnose the layer that failed

Observation Boundary Next check
NewHttpClient returns a config error Proxy URL or incompatible protocol option Validate the full scheme, authority and HTTP/3 choice
Connect timeout or proxy error Reachability or proxy authentication Check secret binding, endpoint, port and network policy
TLS error after connect Proxy TLS or target TLS Separate proxy scheme from target scheme; keep verification enabled
Target returns 401, 403 or a challenge Origin policy or credentials Preserve the body classification and check the permitted request
Status is 2xx but body validation fails Wrong or incomplete result Reject the record and inspect the redacted body

The IP response validates an observed egress body. The target marker validates useful output for this sample. In production, replace the marker with the required JSON field, page value or record key. A proxy supplies a route; it does not guarantee access, solve a challenge, or make a raw client behave like a full browser.

For a Python Requests-style workflow, see the Python Requests proxy guide. For browser contexts, use Playwright proxy setup. The proxy rotation guide covers continuity decisions across clients.

Sources and further reading

Sign in ↗

Keep reading

Amazon Scraper API vs Proxy: Choose a Product Data Source

Distinguish authorized Amazon APIs, licensed product data and proxy-based page checks by record quality and access rights.

Read guide →

Apify Custom Proxy Setup: Use Your Own Residential Route

Connect a buyer-owned proxy to an Apify Actor, keep the session boundary clear, and validate records instead of counting requests.

Read guide →

Australia Residential Proxies: Verify the AU Exit and State-Level Result

Separate Australian egress from en-AU content, AUD pricing, GST display, postcode validation and the state delivery context.

Read guide →