# Golang HTTP Client Proxy: Auth, SOCKS5

> Set a proxy on Go http.Client: Transport.Proxy, HTTP_PROXY and NO_PROXY, SOCKS5, per-request rotation, CONNECT auth, and the errors Go returns.

[Home](https://quanticdata.io/)/[Blog](https://quanticdata.io/blog/)/Golang HTTP Client Proxy: Auth, SOCKS5

# Golang HTTP Client Proxy: Auth, SOCKS5

ProxiesSep 14, 2026·11 min read·By [Aldo Morese](https://quanticdata.io/about/), founder of QuanticData

What a plain Go http.Client gets through a proxy: of twelve public listing pages fetched on 14 September 2026 from US residential exits with no JavaScript, six returned the complete HTML (Wikipedia, Etsy, Booking.com, GitHub Trending, Hacker News, Product Hunt), three returned a partial page or an empty shell (Walmart, Zillow, Target) and three answered 403 (eBay, Indeed, Yelp)

On this page [Three ways to set a proxy, and when each is right](/blog/golang-http-client-proxy/#three-ways-to-set-a-proxy-and-when-each-is-right) [Proxy authentication: Basic, and the 407 you never get to read](/blog/golang-http-client-proxy/#proxy-authentication-basic-and-the-407-you-never-get-to-read) [SOCKS5 with the standard library, no extra package](/blog/golang-http-client-proxy/#socks5-with-the-standard-library-no-extra-package) [Rotating the proxy per request without leaking connections](/blog/golang-http-client-proxy/#rotating-the-proxy-per-request-without-leaking-connections) [HTTP_PROXY, NO_PROXY and the environment that is read exactly once](/blog/golang-http-client-proxy/#http-proxy-no-proxy-and-the-environment-that-is-read-exactly) [The errors Go returns, and what each one means](/blog/golang-http-client-proxy/#the-errors-go-returns-and-what-each-one-means) [What a plain Go client actually gets: 12 pages measured](/blog/golang-http-client-proxy/#what-a-plain-go-client-actually-gets-12-pages-measured) [Bandwidth, and when to stop writing client code](/blog/golang-http-client-proxy/#bandwidth-and-when-to-stop-writing-client-code)

Go has no client.SetProxy. The proxy is a property of the Transport, not of the request, and that single fact explains the rest: why the environment variable sometimes does nothing, why credentials go in the URL, why a 407 on an HTTPS target arrives as a bare error string, and why rotating exits per request is four lines rather than a library.

## Three ways to set a proxy, and when each is right

The standard library gives you exactly one hook, `Transport.Proxy`, a function called once per connection with the request. Everything else is a wrapper around it.

The first way is the environment. Go's `DefaultTransport` uses `ProxyFromEnvironment`, so an exported variable is picked up with no code change at all:

```
export HTTPS_PROXY="http://USER:PASSWORD@proxy.example.net:8080"
export HTTP_PROXY="http://USER:PASSWORD@proxy.example.net:8080"
export NO_PROXY="127.0.0.1,localhost,.internal.example.com"
go run ./cmd/crawler
```

The second way is an explicit client, which is what you want in anything long-lived: it does not depend on how the process was started, and it lets you size the connection pool for scraping instead of for a CLI tool.

```
proxyURL, err := url.Parse("http://USER:PASSWORD@proxy.example.net:8080")
if err != nil {
    log.Fatal(err)
}

tr := &http.Transport{
    Proxy:               http.ProxyURL(proxyURL),
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 10,
    TLSHandshakeTimeout: 10 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 30 * time.Second}

resp, err := client.Get("https://example.com/")
```

The third way is to reassign `http.DefaultTransport`. It works, it is one line, and it changes the behaviour of every package in your binary that quietly uses `http.Get` or `http.DefaultClient` — including your telemetry and your cloud SDKs. Use it in a throwaway script, never in a service.

| Method | Scope | Good for | Trap |
| --- | --- | --- | --- |
| HTTP_PROXY / HTTPS_PROXY | Whole process | CLI tools, corporate egress, containers | Read once per process; ignored for localhost |
| Transport.Proxy with ProxyURL | One client | Services, scrapers, anything with a pool | A second client with no Proxy silently bypasses it |
| Reassigning DefaultTransport | Whole binary | Scripts | Every dependency inherits it, including SDKs |
| Your own func(*http.Request) | Per request | Rotation, per-target routing | Each distinct URL opens its own connection pool |

## Proxy authentication: Basic, and the 407 you never get to read

If the proxy URL carries a userinfo component, the Transport turns it into a `Proxy-Authorization: Basic` header for you. It does this in both directions: on plain `http://` targets the header is added to every request sent down the proxied connection, and on `https://` targets it is added to the CONNECT request that opens the tunnel. You do not write that header yourself, and in fact you cannot: a `Proxy-Authorization` header you set on `req.Header` travels inside the tunnel to the origin server, where it is meaningless.

Credentials with symbols in them must be percent-encoded, which is the single most common cause of a mystifying 407. Build the URL with `url.UserPassword` and Go encodes it correctly:

```
proxy := &url.URL{
    Scheme: "http",
    Host:   "proxy.example.net:8080",
    User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
}

tr := &http.Transport{
    Proxy: http.ProxyURL(proxy),
    OnProxyConnectResponse: func(ctx context.Context, p *url.URL, req *http.Request, res *http.Response) error {
        if res.StatusCode != 200 {
            log.Printf("CONNECT %s -> %s", req.URL.Host, res.Status)
        }
        return nil
    },
}
```

That hook matters more than it looks. When a CONNECT gets a non-200 answer, the Transport closes the connection and returns an error built from the status text alone — you get `Proxy Authentication Required` as an error string, with no status code, no headers and no body, because there is no `*http.Response` to hand back. `OnProxyConnectResponse`, added in Go 1.20, is the only place where you can see what the proxy actually said. On a plain `http://` target there is no tunnel, so the 407 comes back as an ordinary response you can read normally. Our field guide to that status code is in ../how-to-fix-407-proxy-authentication-required/, and the CONNECT-level failures have their own walkthrough in ../err-tunnel-connection-failed/.

## SOCKS5 with the standard library, no extra package

You do not need `golang.org/x/net/proxy` to send HTTP traffic over SOCKS5. Since Go 1.9 the Transport accepts a `socks5://` proxy URL directly, and since Go 1.23 it also accepts `socks5h://` — with the two treated identically, meaning the hostname is resolved by the proxy and not by your machine. Username and password are read from the same userinfo component as HTTP proxies and negotiated with the username/password auth method.

```
proxy := &url.URL{
    Scheme: "socks5h",
    Host:   "proxy.example.net:1080",
    User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
}
client := &http.Client{
    Transport: &http.Transport{Proxy: http.ProxyURL(proxy)},
    Timeout:   30 * time.Second,
}
```

The limit is that `net/http` only ever asks the SOCKS proxy for a TCP connection. If you need UDP, or you are proxying something that is not HTTP, dial through `x/net/proxy` yourself. Our [SOCKS5 endpoints](https://quanticdata.io/socks5-proxies/) answer on the same credentials as the HTTP ones, so switching scheme is a one-character change in the code above.

## Rotating the proxy per request without leaking connections

Because `Proxy` is a function of the request, rotation needs no third-party middleware. Return a different URL per call and keep one client for the whole program:

```
var gateways = []string{
    "gw-1.example.net:8080",
    "gw-2.example.net:8080",
    "gw-3.example.net:8080",
}
var counter uint64

func rotate(req *http.Request) (*url.URL, error) {
    i := atomic.AddUint64(&counter, 1)
    return &url.URL{
        Scheme: "http",
        Host:   gateways[i%uint64(len(gateways))],
        User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
    }, nil
}

client := &http.Client{
    Transport: &http.Transport{
        Proxy:               rotate,
        MaxIdleConns:        200,
        MaxIdleConnsPerHost: 4,
    },
    Timeout: 30 * time.Second,
}
```

Two things follow from how the Transport caches connections, and both bite people who build this in a hurry. The idle-connection pool is keyed by the triple of proxy URL, target scheme and target address, so every distinct proxy string you return gets its own pool — and `MaxIdleConnsPerHost` defaults to 2. With sticky-session usernames, where each session is a different username on the same gateway, you are minting a new pool entry per session and then abandoning it; call `client.CloseIdleConnections()` when you retire a batch of sessions, or keep the number of live sessions bounded on purpose.

The second is the mistake underneath most "my Go scraper eats file descriptors" reports: building a fresh `http.Transport` inside the request loop. A Transport is a connection pool, it is designed to be created once and shared across goroutines, and one per request means zero reuse plus a slow leak. One client, one Transport, a rotating `Proxy` func. If you want the rotation handled on the far side instead, our [rotating endpoint](https://quanticdata.io/rotating-proxies/) gives a new exit per request behind a single host and a single credential, so the Go side stays a two-line `ProxyURL`.

## HTTP_PROXY, NO_PROXY and the environment that is read exactly once

Four behaviours of `ProxyFromEnvironment` account for nearly every "the environment variable does nothing" thread, and all four are in the standard library source rather than in folklore.

- **The environment is read once per process.** The parsed configuration is cached behind a `sync.Once` on first use, so calling `os.Setenv("HTTP_PROXY", ...)` after your program's first HTTP request has no effect whatsoever. Set it before you start, or set `Transport.Proxy` explicitly.

- **Lowercase wins.** When both `http_proxy` and `HTTP_PROXY` are present, the lowercase one takes precedence — the reverse of what most people assume.

- **localhost and loopback are never proxied.** If the request host is localhost or a loopback address, with or without a port, Go returns no proxy as a documented special case. Testing your proxy setup against a local server will always look like the proxy is being ignored, because it is.

- **CGI processes ignore HTTP_PROXY.** If `REQUEST_METHOD` is set in the environment, Go treats the process as a CGI handler and refuses to apply `HTTP_PROXY`, because a remote client could otherwise set it through the Proxy header. That is the httpoxy vulnerability, closed at the library level.

`NO_PROXY` is a comma-separated list, and its grammar is more capable than the one-host-per-entry version most people use. Each entry can be a domain, an IP prefix, a CIDR block, or a single asterisk.

| NO_PROXY entry | Matches | Does not match |
| --- | --- | --- |
| foo.com | foo.com and bar.foo.com | notfoo.com |
| .y.com | x.y.com | y.com |
| 10.0.0.0/8 | Any address in that block | Hostnames that resolve into it |
| 1.2.3.4:80 | That address on that port only | 1.2.3.4:443 |
| * | Everything: proxying is disabled | — |

## The errors Go returns, and what each one means

Go's proxy errors are terse, and two of them are terse in a way that hides the status code. This table is the translation.

| What you see | What happened | Where to look |
| --- | --- | --- |
| proxyconnect tcp: dial tcp …: i/o timeout | The TCP connection to the proxy itself never opened | Port, firewall, egress rules. The target is not involved yet |
| proxyconnect tcp: lookup …: no such host | The proxy hostname does not resolve | Copy the host from your dashboard; check DNS before blaming auth |
| Proxy Authentication Required | A 407 on the CONNECT, returned as a bare status text | Percent-encoding of the password, IP allowlist, exhausted plan |
| Forbidden / Bad Gateway (bare, no code) | Non-200 CONNECT: the proxy refused or could not reach the target | OnProxyConnectResponse to see the real status and headers |
| EOF immediately after CONNECT | The proxy accepted the TCP connection then closed it | Wrong port for the protocol, or a plan with no bandwidth left |
| tls: failed to verify certificate | The TLS handshake is with something that is not your target | An intercepting proxy, or an https:// scheme on an HTTP proxy port |
| context deadline exceeded (Client.Timeout exceeded while awaiting headers) | The client deadline covered CONNECT, TLS and the wait for headers | Raise Client.Timeout, or use per-attempt contexts |

One nuance worth internalising: `http.Client.Timeout` is a deadline on the whole exchange, including the proxy handshake and the reading of the body. A 30-second timeout on a residential exit is not generous; it is the budget for a CONNECT, a TLS handshake, a request and a full HTML download over a home line.

## What a plain Go client actually gets: 12 pages measured

The interesting question is not how to attach a proxy. It is how far a proxied HTTP client gets before you need a browser. On 14 September 2026, between 05:33 and 05:40 UTC, we fetched twelve public listing pages twice each through US residential exits — once as a pure HTTP client with no JavaScript, which is what `net/http` is, and once fully rendered — and compared the word counts. Each pair was one call to our own [SEO audit endpoint](https://quanticdata.io/seo-audit/), which exists precisely to answer this question.

| Page | Words, no JS | Words, rendered | What a Go client gets |
| --- | --- | --- | --- |
| en.wikipedia.org (article) | 6,306 | 6,383 | Everything |
| etsy.com (category) | 5,046 | 5,188 | Everything |
| booking.com (city page) | 4,437 | 4,409 | Everything |
| github.com/trending | 2,248 | 2,248 | Everything, byte for byte |
| news.ycombinator.com | 603 | 603 | Everything |
| producthunt.com | 562 | 676 | Most of it; 83% of the words |
| walmart.com (category) | 823 | 2,838 | Copy and FAQ, not the product grid |
| zillow.com (city listings) | 137 | 403 on render | Thin page, but with an ItemList in the structured data |
| target.com (category) | 27 | 64 | An empty shell: no title, no canonical |
| ebay.com (category) | 403 | 403 | Nothing |
| indeed.com (job search) | 403 | 403 | Nothing |
| yelp.com (search) | 403 | 403 | Nothing |

Six of twelve returned the complete page to a client that cannot run a line of JavaScript. Three returned something partial — Walmart's category page gives you 29% of its rendered words, and the missing 71% is the product grid, which is the only part anyone wants. Three refused outright with a 403 before any HTML existed to parse.

Two honest caveats. First, those fetches used a browser-grade TLS fingerprint; Go's own ClientHello from `crypto/tls` is distinctive enough to be classified on sight, so a stock `net/http` client will do the same or worse on the hostile half, not better. Second, the Zillow row is a useful inversion: the plain HTTP fetch succeeded with a usable `ItemList` in the structured data while the rendered fetch drew a captcha. More machinery is not automatically more access, and the structured data block is often the shortest path to the fields you were going to parse out of the DOM anyway.

## Bandwidth, and when to stop writing client code

Residential traffic is metered by the gigabyte, and HTML is heavier than it looks once images, fonts and analytics beacons are counted — which is why a Go scraper that only ever reads `resp.Body` is cheaper per page than a headless browser doing the same job. We put real numbers on that in ../how-much-proxy-data-do-i-need/; the short version is that page weight, not request count, is what your bill tracks. Our [residential pool](https://quanticdata.io/residential-proxies/) starts at $0.80/GB on the Basic tier with no subscription, and rotating exits start at $0.50/GB.

Write the client yourself for the six-out-of-twelve half: those pages are a Transport, a rotating `Proxy` func and `golang.org/x/net/html`, and nothing more. For the other half — the 403s, the shells, the grids that only exist after JavaScript — the work is no longer Go code, it is fingerprinting and rendering maintenance. That is what our [scraping API](https://quanticdata.io/web-scraping-api/) is for: one HTTP call from the same `http.Client` you already built, clean Markdown or structured JSON back, from $0.0002 per page and $0.001 with rendering, billed only on success. Every account gets $2 of free API usage a month, which is several thousand pages before you decide anything.

### Sources & further reading

- [Go standard library: net/http Transport (Proxy field and CONNECT behaviour)](https://pkg.go.dev/net/http#Transport)

- [Go standard library: http.ProxyFromEnvironment](https://pkg.go.dev/net/http#ProxyFromEnvironment)

- [golang.org/x/net/http/httpproxy: NO_PROXY grammar and CGI handling](https://pkg.go.dev/golang.org/x/net/http/httpproxy)

- [httpoxy: a CGI application vulnerability](https://httpoxy.org/)

- [Stack Overflow: Setting up proxy for HTTP client](https://stackoverflow.com/questions/14661511/setting-up-proxy-for-http-client)

- [Eli Bendersky: Go and proxy servers, part 3 - SOCKS proxies](https://eli.thegreenplace.net/2022/go-and-proxy-servers-part-3-socks-proxies/)

- [RFC 9110, section 15.5.8: 407 Proxy Authentication Required](https://www.rfc-editor.org/rfc/rfc9110#name-407-proxy-authentication-re)

## FAQ

Quick answers on golang http client proxy.

[Something else? Ask us →](mailto:hello@quanticdata.io)

### How do I set a proxy for a single request in Go?

There is no per-request field. `Transport.Proxy` is a `func(*http.Request) (*url.URL, error)`, so you route inside that function: inspect `req.URL.Host`, or read a value you put on the request context, and return the proxy URL you want for that call. The function runs once per new connection, and the resulting connection is pooled per proxy URL.

### Why is my HTTP_PROXY variable being ignored?

Four documented reasons, in order of likelihood: the target is localhost or a loopback address, which Go never proxies; the variable was set with `os.Setenv` after the first request, and the environment is parsed once and cached; a lowercase `http_proxy` is also set and takes precedence; or the process has `REQUEST_METHOD` in its environment, which makes Go treat it as CGI and ignore `HTTP_PROXY` entirely.

### Does Go support SOCKS5 proxies without an extra package?

Yes. `Transport.Proxy` has accepted `socks5://` URLs since Go 1.9 and `socks5h://` since Go 1.23, and treats the two the same, so hostnames are resolved by the proxy. Username and password come from the URL userinfo. You only need `golang.org/x/net/proxy` for non-HTTP traffic or UDP, which `net/http` never asks for.

### How do I pass proxy credentials in Go?

Put them in the proxy URL, ideally via `url.UserPassword` so that symbols are percent-encoded correctly. Go then emits `Proxy-Authorization: Basic` itself, on the CONNECT for HTTPS targets and on each request for plain HTTP targets. Setting that header on your own request object does not work for HTTPS: it goes through the tunnel to the origin server.

### Why do I get "Proxy Authentication Required" with no status code?

Because the 407 was answered to a CONNECT request, and a failed CONNECT never produces an `*http.Response` for the caller — the Transport closes the connection and returns an error made from the status text alone. Register `OnProxyConnectResponse` (Go 1.20 and later) to inspect the actual response, headers included, before the error is built.

### Do I need one http.Client per goroutine when rotating proxies?

No, and doing it is the usual cause of descriptor exhaustion. An `http.Client` and its Transport are safe for concurrent use and exist to pool connections. Share one, put the rotation in the `Proxy` function, and raise `MaxIdleConnsPerHost` from its default of 2 — the idle pool is keyed by proxy URL, so every exit you rotate through keeps its own small set of connections.

## Proxies that answer a Go client on the first request

HTTP and SOCKS5 on the same credentials, rotating or sticky exits, country targeting per request, and a scraping API for the pages that answer 403. Every account gets $2 of free usage each month.

[Start free — $2/month included](https://quanticdata.io/signup/)[Explore Residential Proxies from $0.80/GB](https://quanticdata.io/residential-proxies/)

## Related reading

[Proxies Shadowrocket Proxy Setup: The iPhone Guide Shadowrocket is a routing engine, not a VPN subscription. The add-server screen takes two minutes; the rules decide whether it is worth the money. Setup, config syntax, and measured exits. Read →](https://quanticdata.io/blog/shadowrocket-proxy-setup/) [Proxies Best Mobile Proxies in 2026, Tested Fourteen mobile proxy providers, priced from their own pages on 13 September 2026 - four publish a usable per-GB rate, three of the biggest publish none at all, and the rest sell per IP or per day. Plus 98 requests through our own 4G/5G network with the raw rows attached: 40/40 success, 100% country match, 23 carrier ASNs, 1.74 s median TTFB, 15 of 16 sticky sessions holding one IP. No ban rates, no success-rate claims, no number we did not measure. Read →](https://quanticdata.io/blog/best-mobile-proxies-tested/) [Proxies Best Residential Proxies in 2026, Tested Thirteen residential proxy providers, priced from their own pages on 8 September 2026 - eleven publish a rate, two could not be verified, and every sub-dollar figure on the market turns out to carry a commitment. Plus 170 requests through our own residential pool with the raw rows attached: 49/50 success, 50/50 country match, 41 ASNs, 1.26 s median TTFB, 20 of 20 sticky sessions holding one IP, a median fraud score of 2 - and the finding that 95% of our exits are catalogued as other networks' nodes too. Read →](https://quanticdata.io/blog/best-residential-proxies-tested/)

## Also on this site

Quantic**Data**

Residential proxies & web data APIs for AI.

#### Proxies

- [Residential Basic](https://quanticdata.io/residential-proxies/#basic)

- [Residential Premium](https://quanticdata.io/residential-proxies/#plans)

- [Cheap Residential](https://quanticdata.io/cheap-residential-proxies/)

- [Mobile Proxies](https://quanticdata.io/mobile-proxies/)

- [Datacenter Proxies](https://quanticdata.io/datacenter-proxies/)

- [ISP Proxies](https://quanticdata.io/isp-proxies/)

- [Rotating Proxies](https://quanticdata.io/rotating-proxies/)

- [Sneaker Proxies](https://quanticdata.io/sneaker-proxies/)

- [SOCKS5 Proxies](https://quanticdata.io/socks5-proxies/)

- [IPv6 Proxies](https://quanticdata.io/ipv6-proxies/)

- [Proxy locations](https://quanticdata.io/proxies/)

#### Data APIs

- [MCP Server](https://quanticdata.io/mcp-server/)

- [Web Scraper API](https://quanticdata.io/web-scraping-api/)

- [SERP API](https://quanticdata.io/serp-api/)

- [Collectors](https://quanticdata.io/collectors/)

- [Web Data for AI](https://quanticdata.io/web-data-api-for-ai/)

- [Quantic AI](https://quanticdata.io/ai-web-scraping-service/)

- [Crawl & Map](https://quanticdata.io/crawl-map/)

- [SEO Audit](https://quanticdata.io/seo-audit/)

#### Use cases

- [Company data](https://quanticdata.io/scrape-company-data/)

- [Price monitoring](https://quanticdata.io/competitor-price-monitoring/)

- [Market research](https://quanticdata.io/market-research-data/)

- [Real estate data](https://quanticdata.io/real-estate-data-scraping/)

- [Scrape job postings](https://quanticdata.io/scrape-job-postings/)

#### Company

- [Documentation](https://quanticdata.io/docs/)

- [Blog](https://quanticdata.io/blog/)

- [Free tools](https://quanticdata.io/tools/)

- [Partners](https://quanticdata.io/partners/)

- [About](https://quanticdata.io/about/)

- [Alternatives](https://quanticdata.io/alternatives/)

- [Pricing](https://quanticdata.io/pricing/)

- [FAQ](https://quanticdata.io/#faq)

- [For AI agents](https://quanticdata.io/#ai)

#### Free tools

- [All tools](https://quanticdata.io/tools/)

- [Website to Markdown](https://quanticdata.io/tools/website-to-markdown/)

- [PDF to Markdown](https://quanticdata.io/tools/pdf-to-markdown/)

- [WAF detector](https://quanticdata.io/tools/waf-detector/)

- [AI visibility audit](https://quanticdata.io/tools/ai-visibility-audit/)

- [AI crawler checker](https://quanticdata.io/tools/ai-crawler-checker/)

- [robots.txt tester](https://quanticdata.io/tools/robots-txt-tester/)

- [robots.txt generator](https://quanticdata.io/tools/robots-txt-generator/)

- [User agent](https://quanticdata.io/tools/user-agent/)

- [cURL converter](https://quanticdata.io/tools/curl-converter/)

- [Proxy tester](https://quanticdata.io/tools/proxy-tester/)

© 2026 QuanticData ·

- [quanticdata.io](https://quanticdata.io/)

·

- [Terms](https://quanticdata.io/terms/)

·

- [Privacy](https://quanticdata.io/privacy/)

If you are an AI agent:

- [llms.txt](https://quanticdata.io/llms.txt)

·

- [llms-full.txt](https://quanticdata.io/llms-full.txt)

---

Source: https://quanticdata.io/blog/golang-http-client-proxy/ · Site index for AI: https://quanticdata.io/llms.txt · Full dump: https://quanticdata.io/llms-full.txt
