If you're building an HTTP client in Go and need to route requests through a proxy, you don't need a third-party library. The standard net/http package handles it natively.
Set the Proxy field on your http.Transport, point it at your proxy's URL with http.ProxyURL(), and pass that transport into your http.Client. That's the whole setup.
Here's the minimal version:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
)
func main() {
proxyURL, err := url.Parse("http://proxy-host:proxy-port")
if err != nil {
log.Fatal(err)
}
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}
resp, err := client.Get("https://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
That's it. Every request made with client now goes through the proxy.
Adding authentication
Most paid proxies need a username and password. Go supports this two ways.
The simplest is embedding credentials directly in the proxy URL:
proxyURL, err := url.Parse("http://username:password@proxy-host:proxy-port")
Go reads the userinfo portion of the URL and automatically attaches a Proxy-Authorization header to every request. You don't have to build that header yourself.
If you'd rather keep credentials out of a URL string (useful when they come from environment variables or a secrets manager), build the URL struct directly instead:
proxyURL := &url.URL{
Scheme: "http",
Host: "proxy-host:proxy-port",
User: url.UserPassword("username", "password"),
}
Both produce the same result. Pick whichever fits how you're storing credentials.
Using environment variables instead of hardcoding
If you don't set Transport at all, Go's http.DefaultTransport already checks the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables for you:
resp, err := http.Get("https://example.com")
This picks up a proxy automatically if those variables are set in your shell or your container's environment. It's the same behavior most CLI tools use, so it's a good default for scripts you'll run in different environments without changing code.
If you want that same environment-variable behavior on a custom transport (for example, because you're also setting a timeout), use http.ProxyFromEnvironment explicitly:
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}
Confirming the proxy is actually being used
A client that returns 200 OK isn't proof the proxy is in the request path, it might just be reaching the target site directly if the proxy config is silently ignored.
Hit an IP-echo endpoint with and without the proxy, and compare:
resp, err := client.Get("https://api.ipify.org")
If the returned IP matches your proxy's IP (not your machine's), the proxy is doing its job. If it matches your own connection, something in the transport setup isn't being applied, usually a Transport created after the client, or a client reused from http.DefaultClient instead of your custom one.
Handling the errors you'll actually hit
Three errors show up most often once you're running this against a real proxy:
proxyconnect tcp: dial tcp ...: connect: connection refused The proxy host or port is wrong, or the proxy is down. Check the address before debugging anything else in your code.
407 Proxy Authentication Required Your credentials are missing or wrong. If you're using the userinfo-in-URL approach, double-check the URL was parsed correctly, a password containing @ or : needs to be URL-encoded or it'll break the parse.
Requests that hang and eventually time out Set an explicit timeout so a dead proxy fails fast instead of hanging your program:
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
Without this, a client using a bad proxy can hang far longer than you'd expect, since Go's default transport has no request-level timeout.
Putting it together
For most Go projects, this is enough:
package main
import (
"log"
"net/http"
"net/url"
"time"
)
func newProxyClient(rawProxyURL, username, password string) (*http.Client, error) {
proxyURL, err := url.Parse(rawProxyURL)
if err != nil {
return nil, err
}
proxyURL.User = url.UserPassword(username, password)
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}, nil
}
func main() {
client, err := newProxyClient("http://proxy-host:proxy-port", "username", "password")
if err != nil {
log.Fatal(err)
}
resp, err := client.Get("https://example.com")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}
One http.Client, built once, reused across every request that needs to go through the proxy. No dependencies beyond the standard library.
If you're testing this against a real proxy pool rather than a single static proxy, keep in mind that providers billing per successfully authenticated IP expect exactly this kind of username/password setup, so the code above should work against most of them without changes beyond the host, port, and credentials.