Proxy authentication in Go’s net/http, HTTP and SOCKS5

The standard library does all of it, SOCKS5 included. What trips people up: rotation that doesn’t rotate, passwords that break the URL, and requests that end in EOF.

6 min readTested on Go 1.25
With keep-alive, three requests through a rotating proxy leave from the same IP; with DisableKeepAlives each request gets a new IP.Three requests, one rotating gatewaymain.goDefault Transportkeep-alive onreq 131.134.73.142req 231.134.73.142req 331.134.73.142main.goOne IP per requestDisableKeepAlives: truereq 1186.219.138.87req 2dropped → retryreq 3103.131.180.109Measured with Go 1.23 on our gateway

In short

Build the proxy URL with url.UserPassword and set it on an http.Transport with http.ProxyURL. For a new IP on every request through a rotating gateway, also set DisableKeepAlives: true. Otherwise Go quietly reuses the same exit.

socks5:// · no extra packageDisableKeepAlives · new IP per request

An http.Transport asks its Proxy function which proxy to use for each request. http.ProxyURL returns the same proxy every time, and the user info in that URL (username and password) is what Go sends to the proxy.

For an http:// target Go sends the request to the proxy with a Proxy-Authorization header. For an https:// target it first asks the proxy to open a tunnel with CONNECT, authenticated the same way, and speaks TLS to the website through it. You don’t write any of that; the proxy scheme stays http:// in both cases.

SOCKS5 works the same way: give the URL the socks5 scheme and net/http handles the handshake and the password itself. We ran both against our gateway with Go 1.25, without importing anything outside the standard library.

go
proxyURL := &url.URL{
	Scheme: "http", // "socks5" works the same way
	User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
	Host:   "via.proxies.vision:9000", // SOCKS5: 11000
}
client := &http.Client{
	Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
	Timeout:   30 * time.Second,
}
resp, err := client.Get("https://ipinfo.io/json")

Build the URL, don’t format it

A password with @, : or / breaks a URL put together with fmt.Sprintf. url.UserPassword escapes those characters, so the URL always parses.

Always set a timeout

The zero-value http.Client waits forever. A residential exit can stall, so give every client a Timeout.

A rotating gateway picks a new exit IP when a connection opens, not for every request on it. Go’s Transport keeps connections alive and sends the next request through the tunnel that is already open. So with a default client, every request after the first leaves through the same IP, even though the gateway rotates.

We ran three requests through one client, then three with keep-alive disabled:

ClientRequest 1Request 2Request 3
Default Transport152.55.177.69152.55.177.69152.55.177.69
DisableKeepAlives: true209.214.13.74172.59.191.1872.168.144.6
Fig. 1 — Go 1.25 against via.proxies.vision:9000, October 14, 2026.

For one IP per request, open a new connection per request. For the same IP on purpose, for a login or a checkout, keep keep-alive on and add a session label to the username instead: USER-session-checkout1-lifetime-10 on the sticky port 10000 holds one IP for ten minutes, across connections and even across processes.

go
Transport: &http.Transport{
	Proxy:             http.ProxyURL(proxyURL),
	DisableKeepAlives: true, // one connection per request = one IP per request
},

Residential exits are real home connections, and now and then one drops in the middle of a request. Go reports it as Get "https://…": EOF. It isn’t your code and it isn’t the proxy rejecting you: the next attempt gets a healthy exit. Retry network errors a couple of times with a short pause.

go
for i := 0; i < attempts; i++ {
	resp, err := client.Get(target)
	if err != nil {
		lastErr = err // EOF, reset, timeout: try a fresh exit
		time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
		continue
	}
	// …read the body, return on success
}

Reading proxy errors

When the proxy refuses a request, Go returns an error before any response. Go shows the proxy’s reason phrase, not the status number.

Go’s error ends withCauseFix
Proxy Authentication RequiredWrong username or password (407)Copy both again from the dashboard
Invalid RequestA value in the username the gateway can’t read (403)Two-letter country codes, cities without spaces, sessions only on the sticky port
Can Not Fulfill TargetingNo exit matches all your filters (465)Remove the narrowest filter (city, network)
EOFA residential exit droppedRetry

Filters fail closed

An impossible combination, such as a city that doesn’t exist in the chosen country, fails instead of quietly giving you another location. City names go in lower case, without spaces or accents: São Paulo is saopaulo. The generator below rewrites them for you. A key the gateway doesn’t know, such as -cc-US, is ignored without an error, so check where your exits land.

Seeing ERR_TUNNEL_CONNECTION_FAILED in a headless browser instead? That’s the same refusal in Chromium’s words: every cause, tested.

A setup that works

Pick protocol, rotation and location. The program compiles as shown, retries dropped connections, and needs nothing outside the standard library.

Generate your Go client

compiles as shown · Go 1.23

Protocol
IP rotation

Username sent to the proxy: PROXY_USER-country-de

gomain.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	user := os.Getenv("PROXY_USER") + "-country-de"

	proxyURL := &url.URL{
		Scheme: "http",
		// url.UserPassword escapes special characters in the password.
		User: url.UserPassword(user, os.Getenv("PROXY_PASS")),
		// Rotating ports pick a new exit per connection: HTTP 9000, SOCKS5 11000.
		Host: "via.proxies.vision:9000",
	}

	client := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
			// Without this, Go reuses the open tunnel and every request leaves
			// through the same IP. One connection per request = one IP per request.
			DisableKeepAlives: true,
		},
		Timeout: 30 * time.Second,
	}

	for i := 1; i <= 3; i++ {
		body, err := fetch(client, "https://ipinfo.io/json", 3)
		if err != nil {
			fmt.Println("request failed:", err)
			continue
		}
		fmt.Printf("request %d: %s\n", i, body)
	}
}

// fetch retries on network errors: residential exits occasionally drop a
// connection mid-request (Go reports EOF), and the next attempt gets a
// healthy one. Proxy refusals (403, 407, 465) come back as errors too.
func fetch(client *http.Client, target string, attempts int) ([]byte, error) {
	var lastErr error
	for i := 0; i < attempts; i++ {
		resp, err := client.Get(target)
		if err != nil {
			lastErr = err
			time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
			continue
		}
		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			lastErr = err
			continue
		}
		if resp.StatusCode >= 500 {
			lastErr = fmt.Errorf("target answered %s", resp.Status)
			continue
		}
		return body, nil
	}
	return nil, lastErr
}

Run it with PROXY_USER=… PROXY_PASS=… go run main.go. No third-party packages needed, SOCKS5 included.

Do I need golang.org/x/net/proxy for SOCKS5?
Not for HTTP clients. net/http's Transport accepts socks5:// proxy URLs with username and password directly, and we tested that with Go 1.25. You need x/net/proxy only for raw TCP connections outside net/http.
Why does my rotating proxy return the same IP for every request in Go?
Go keeps the connection to the proxy open and sends the next request through the same tunnel, so the gateway never gets the chance to pick a new exit. Set DisableKeepAlives: true on the Transport for one IP per request, or use a sticky session label when you want the same IP on purpose.
How do I pass a proxy password with special characters?
Build the proxy URL with url.UserPassword(user, pass) instead of formatting a string. It escapes characters like @, : and / so the URL parses correctly.
Does HTTPS work through an HTTP proxy in Go?
Yes. For https:// targets Go opens a CONNECT tunnel through the proxy and sends the Proxy-Authorization header from the URL's user info automatically. Keep the proxy scheme http://.

Residential proxies, pay per GB

HTTP and SOCKS5. Country, city and sticky sessions in the username. No subscription.