Integration · HTTP clients
How to use a proxy with Go net/http
In Go, a proxy is the Proxy field of an http.Transport: a function that returns the proxy URL for each request. Put the credentials in that URL with url.UserPassword and net/http sends them as Proxy-Authorization itself. Because the function sees each request, one client can give every task its own sticky session.
Reviewed 30 Oct 2026
http://USERNAME _pool-arcfast _country-de:PASSWORD@ proxy.arcproxies.io:8000The short answer
http.ProxyURL(u)for one fixed proxy; a customProxyfunc to choose the username per request.- Build the URL with
url.UserPasswordso special characters in the password are escaped for you. OnProxyConnectResponsesees the proxy's answer to CONNECT, which makes a 407 easy to detect witherrors.Is.- Reuse one
http.Client: it is safe for concurrent use, and connections are pooled per proxy URL, credentials included.
Setup
Set up Go with a proxy, step by step
- 01
Send a request through the gateway and check the exit
net/http reads the userinfo from the proxy URL and sends Basic proxy authentication. The echo service returns the exit the target sees.
main.go29 linespackage mainimport ( "fmt" "io" "net/http" "net/url" "time")func main() { proxyURL := &url.URL{ Scheme: "http", User: url.UserPassword("USERNAME_pool-arcfast_country-de", "PASSWORD"), Host: "proxy.arcproxies.io:8000", } client := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, } resp, err := client.Get("https://api.ipify.org?format=json") if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(resp.StatusCode, string(body)) // 200 {"ip":"…"}} - 02
Choose the session per request with a Proxy func
Store a session handle in the request's context; the
Proxyfunc turns it into a sticky username. Requests without a handle use the rotating username. The connection pool is keyed by the full proxy URL, so different handles never share a connection.proxy.go22 linestype sessionKey struct{}// WithSession pins every request made with ctx to one exit IP.func WithSession(ctx context.Context, handle string) context.Context { return context.WithValue(ctx, sessionKey{}, handle)}func arcProxy(r *http.Request) (*url.URL, error) { user := "USERNAME_pool-arcfast_country-de" if h, ok := r.Context().Value(sessionKey{}).(string); ok && h != "" { user += "_session-" + h + "_sesstime-10" } return &url.URL{Scheme: "http", User: url.UserPassword(user, "PASSWORD"), Host: "proxy.arcproxies.io:8000"}, nil}var client = &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ Proxy: arcProxy, MaxIdleConnsPerHost: 16, // the default of 2 is low for concurrent workers },} - 03
Run tasks concurrently, each on its own exit
One client for all goroutines. Each task mints a handle, puts it in the context, and every request it makes leaves from the same IP. A buffered channel caps how many tasks run at once.
main.go37 linesfunc newHandle() string { b := make([]byte, 4) rand.Read(b) // crypto/rand return hex.EncodeToString(b)}func crawlListing(ctx context.Context, start string) error { ctx = WithSession(ctx, newHandle()) for page := 1; page <= 3; page++ { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s?page=%d", start, page), nil) resp, err := client.Do(req) if err != nil { return err } io.Copy(io.Discard, resp.Body) resp.Body.Close() } return nil}func main() { starts := []string{"https://example.com/c/lamps", "https://example.com/c/chairs"} slots := make(chan struct{}, 8) var wg sync.WaitGroup for _, s := range starts { wg.Add(1) go func(start string) { defer wg.Done() slots <- struct{}{} defer func() { <-slots }() if err := crawlListing(context.Background(), start); err != nil { log.Println(start, err) } }(s) } wg.Wait()} - 04
Geo-target with a different username
Country and city are username tokens. Swap the base username per market; cities come from the published list, lowercased without spaces.
geo.go10 linesvar markets = map[string]string{ "de-cologne": "USERNAME_pool-arcfast_country-de_city-cologne", "jp-osaka": "USERNAME_pool-arcfast_country-jp_city-osaka", "hk": "USERNAME_pool-arcfast_country-hk",}func clientFor(username string) *http.Client { u := &url.URL{Scheme: "http", User: url.UserPassword(username, "PASSWORD"), Host: "proxy.arcproxies.io:8000"} return &http.Client{Timeout: 30 * time.Second, Transport: &http.Transport{Proxy: http.ProxyURL(u)}}} - 05
Detect 407 and retry the rest
OnProxyConnectResponseruns when the proxy answers a CONNECT. Returning a sentinel error for 407 lets the caller stop witherrors.Isinstead of matching strings. Retry 429, 5xx and timeouts with backoff on a new handle.retry.go31 linesvar errProxyAuth = errors.New("proxy rejected the credentials (407)")var transport = &http.Transport{ Proxy: arcProxy, OnProxyConnectResponse: func(ctx context.Context, proxyURL *url.URL, req *http.Request, res *http.Response) error { if res.StatusCode == http.StatusProxyAuthRequired { return errProxyAuth } return nil },}var client = &http.Client{Timeout: 30 * time.Second, Transport: transport}func fetch(url string) ([]byte, error) { for attempt := 0; attempt < 4; attempt++ { req, _ := http.NewRequestWithContext(WithSession(context.Background(), newHandle()), http.MethodGet, url, nil) resp, err := client.Do(req) if errors.Is(err, errProxyAuth) { return nil, err // wrong credentials: retrying will not help } if err == nil { body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 429 && resp.StatusCode < 500 { return body, readErr } } time.Sleep(time.Second << attempt) // 1s, 2s, 4s, 8s } return nil, fmt.Errorf("gave up on %s", url)}
What Transport.Proxy supports
- Schemes:
http,https,socks5andsocks5h. An empty scheme meanshttp. Go treatssocks5the same assocks5h, so DNS is always resolved at the proxy. - Credentials: if the proxy URL has userinfo, net/http sends it as
Proxy-Authorization(HTTP proxies) or as SOCKS5 username and password. - HTTPS targets go through an HTTP CONNECT tunnel; TLS runs between your program and the site.
- Environment:
http.ProxyFromEnvironment(the default transport's choice) readsHTTP_PROXY,HTTPS_PROXYandNO_PROXY. An explicitProxyfunc avoids surprises from a stray variable.
Connection pooling decides when the IP changes
The transport pools connections per proxy URL (credentials included) and target. With a rotating username, consecutive HTTPS requests to one host usually reuse one tunnel and therefore one exit; a new IP needs a new connection. With a handle per task, each task has its own pool entry and keeps its exit for _sesstime- minutes.
MaxIdleConnsPerHost defaults to 2, which forces new connections, and new tunnels, under concurrency. Raise it to roughly your worker count. MaxConnsPerHost caps the total per pool entry if you need a hard ceiling. Pace requests to what each site tolerates; handling 429 rate limits covers the backoff side.
The problem
Common pitfalls
Building the proxy URL by string concatenation
A password with @, : or / breaks a hand-built URL. url.UserPassword escapes it correctly.
A new http.Client per request
Each new transport starts with an empty pool, so every request pays for a new tunnel. Share one client; it is safe for concurrent use.
Not draining the body
A connection only goes back to the pool once the body is read to EOF and closed. Undrained bodies leak connections and force new tunnels.
Matching on the error string
For HTTPS targets a 407 currently surfaces as an error reading "Proxy Authentication Required". OnProxyConnectResponse with a sentinel error is sturdier than string matching.
FAQ
Questions
Something not covered? Write to contact@arcproxies.io.
How do I set an authenticated proxy in Go?
Create the URL with &url.URL{Scheme: "http", User: url.UserPassword(user, pass), Host: "host:port"} and set Transport: &http.Transport{Proxy: http.ProxyURL(u)} on your client. net/http sends the credentials as Proxy-Authorization.
Can Go's http.Client use a SOCKS5 proxy?
Yes. Transport.Proxy accepts socks5:// and socks5h:// URLs, and userinfo in the URL is used for SOCKS5 authentication. Go treats both schemes the same, resolving DNS at the proxy.
How do I use a different proxy per request in Go?
Set Transport.Proxy to your own func(*http.Request) (*url.URL, error). It runs for each request, so it can read a value from the request's context and return a different username, for example a sticky session handle per task.
How do I detect a 407 from the proxy in Go?
For HTTPS targets the proxy answers the CONNECT request, which client.Do reports as an error. Set Transport.OnProxyConnectResponse to return your own error when res.StatusCode == 407, then check it with errors.Is.
Sources & review
Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.
- [1]Go: net/http: Transport
- [2]Go: net/http: ProxyURL
- [3]Go: net/url: UserPassword
Next
What people build with it
Integrations
Other http clients
Get started
Get credentials and paste them in.
Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.