Skip to content

Guides · Connect and debug

Go HTTP proxy setup with authentication

Use Go’s net/http client to send an HTTPS request through an authenticated HTTP proxy. The example keeps credentials out of destination headers, preserves TLS verification and puts explicit limits on connection time and response reading.

Short answers

How do I set a proxy on a Go HTTP client?

Create a transport, set its Proxy field with http.ProxyURL, and assign it to an http.Client. This makes the route explicit for that client. The standard transport opens a CONNECT tunnel for an HTTPS destination.

How do I provide proxy authentication?

Assign url.UserPassword to the gateway URL’s User field. Go then supplies proxy authentication. Do not use Request.SetBasicAuth for gateway credentials: that sets destination authentication.

Do I need an additional Go proxy package?

Not for this HTTP CONNECT example. It uses the standard library. Go’s environment proxy function is also built in, but this recipe selects a gateway explicitly so process environment exclusions do not change it.

Start with the generated connection settings

Open the connection builder and select HTTP plus the pool, country and rotation your authorised job needs. Supply the full generated username in PROXY_USER and the proxy password in PROXY_PASSWORD through runtime secret configuration. Keep both values out of source control and shell history. Optional PROXY_SERVER contains just the HTTP gateway address.

The example was compiled and executed with Go 1.27.1 on 1 October 2026. It needs no third-party module. Run go version in the environment that will execute your worker; a locally installed compiler does not establish the version inside a deployment image. Save the program as proxy-check.go and run go run proxy-check.go after configuring its environment.

Use an explicit transport for one echo request

The program clones the standard transport before changing it, leaving other clients’ settings intact. It disables redirect following for this check and validates the echo body. A failing HTTP status is checked explicitly because a completed Go request can return a non-success status with no transport error.

proxy-check.gogo
package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

func check() error {
	server := os.Getenv("PROXY_SERVER")
	if server == "" {
		server = "http://gw.portproof.org:7000"
	}
	proxy, err := url.Parse(server)
	if err != nil || proxy.Scheme != "http" || proxy.Hostname() == "" ||
		proxy.User != nil || (proxy.Path != "" && proxy.Path != "/") ||
		proxy.RawQuery != "" || proxy.Fragment != "" {
		return errors.New("use an HTTP gateway URL without credentials or a path")
	}
	user, password := os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASSWORD")
	if user == "" || password == "" || strings.Contains(user, ":") {
		return errors.New("check runtime proxy credentials; username cannot contain a colon")
	}
	proxy.User = url.UserPassword(user, password)

	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = http.ProxyURL(proxy)
	transport.DialContext = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
	transport.TLSHandshakeTimeout = 5 * time.Second
	transport.ResponseHeaderTimeout = 10 * time.Second
	transport.MaxConnsPerHost = 1
	transport.MaxIdleConns = 1
	transport.MaxResponseHeaderBytes = 16 << 10
	transport.OnProxyConnectResponse = func(_ context.Context, _ *url.URL,
		_ *http.Request, response *http.Response) error {
		if response.StatusCode != http.StatusOK {
			return fmt.Errorf("proxy CONNECT status %d", response.StatusCode)
		}
		return nil
	}
	// Optional approved CA file; default certificate verification stays enabled.
	if file := os.Getenv("PROXY_CA_BUNDLE"); file != "" {
		pem, err := os.ReadFile(file)
		if err != nil {
			return errors.New("cannot read approved CA bundle")
		}
		roots, err := x509.SystemCertPool()
		if err != nil {
			return errors.New("cannot load system trust store")
		}
		if !roots.AppendCertsFromPEM(pem) {
			return errors.New("invalid CA bundle")
		}
		transport.TLSClientConfig = &tls.Config{RootCAs: roots}
	}
	defer transport.CloseIdleConnections()
	client := &http.Client{
		Transport: transport, Timeout: 20 * time.Second,
		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	request, err := http.NewRequest(http.MethodGet, "https://api.portproof.org/v1/echo-ip", nil)
	if err != nil {
		return errors.New("invalid echo URL")
	}
	response, err := client.Do(request)
	if err != nil {
		var certificateError *tls.CertificateVerificationError
		switch {
		case errors.As(err, &certificateError):
			return errors.New("destination TLS certificate verification failed")
		case errors.Is(err, context.DeadlineExceeded):
			return errors.New("request deadline exceeded")
		default:
			// url.Error may contain URLs: expose only a recognised CONNECT status.
			var wrapped *url.Error
			if errors.As(err, &wrapped) && strings.HasPrefix(wrapped.Err.Error(), "proxy CONNECT status ") {
				return wrapped.Err
			}
			return errors.New("connection failed; check gateway, DNS and timeout settings")
		}
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("destination HTTP status %d", response.StatusCode)
	}
	const limit = 64 << 10
	body, err := io.ReadAll(io.LimitReader(response.Body, limit+1))
	if err != nil {
		return errors.New("response read failed or exceeded its deadline")
	}
	if len(body) > limit {
		return errors.New("echo response exceeds 64 KiB")
	}
	var payload struct {
		IP string `json:"ip"`
	}
	if json.Unmarshal(body, &payload) != nil || net.ParseIP(payload.IP) == nil {
		return errors.New("invalid echo JSON")
	}
	fmt.Println("HTTP", response.StatusCode, "exit", payload.IP)
	return nil
}

func main() {
	if err := check(); err != nil {
		fmt.Fprintln(os.Stderr, "Check failed:", err)
		os.Exit(1)
	}
}

The Go transport reference in its source documents the Proxy field, CONNECT callback and connection limits. The callback runs before the transport accepts the tunnel. It reports a gateway status separately from a response returned by the destination, without exposing the proxy URL.

Treat proxy and destination credentials separately

Pass original credential values to url.UserPassword; do not percent-encode them first or concatenate a credential URL by hand. It constructs the user information component safely when values contain URL punctuation. The example refuses a username containing a colon because Basic authentication uses that character to separate username and password. A colon in the password remains part of the password.

Do not add the gateway password with SetBasicAuth or put Proxy-Authorization into the ordinary request headers. The transport gets authentication from the proxy URL and applies it to CONNECT. A destination API token, if your real workflow needs one, belongs to that destination’s authentication mechanism. Do not send it to an unrelated echo service.

The proxy URL stays http:// while the requested URL is https://. These schemes describe different connections. CONNECT carries the destination TLS exchange through the gateway; it does not add TLS to the client’s HTTP connection to that gateway. The destination certificate and hostname must still validate. Optional PROXY_CA_BUNDLE adds a trusted CA file to the system roots; never use an arbitrary certificate file to silence an error.

Decide whether routing is explicit or inherited

The default Go transport uses environment proxy settings. This clone replaces that function with http.ProxyURL(proxy), so HTTP_PROXY, HTTPS_PROXY and NO_PROXY do not select a different route for this client. The local fixture checked conflicting environment values, including an exclusion covering every destination.

If your organisation intentionally configures routing through the process environment, retain http.ProxyFromEnvironment instead and test the deployed process. Its hostname exclusions can send some requests directly. Keep configuration immutable once requests are running; create a separate client when credentials or routing policy need a separate lifecycle. Do not modify the shared default transport to change one job.

Time the full operation and cap the body

The five-second dial and TLS handshake settings and ten-second response-header setting bound particular stages. The twenty-second client timeout also covers response-body reading. Go’s client timeout documentation makes that distinction explicit: returning from Do does not end the timer while the body is still being read.

The program reads at most 64 KiB plus one byte. The extra byte distinguishes an oversized response from one exactly at the limit. Only the expected body reaches JSON decoding. This bounds data retained by the example, not total gateway traffic, TLS overhead or every internal buffer. The response-header cap is a separate setting.

Every obtained response body is closed before the transport’s idle connections are closed. A successful small body is consumed fully, allowing connection reuse in a service. An error body is closed without an unbounded drain. Closing an unread body can sacrifice reuse; preserving that connection is less important than keeping the diagnostic request bounded.

Match each error to the next useful check

Proxy CONNECT status 407
Verify the generated username, password and balance. The 407 guide separates rejected authentication from other gateway conditions.
Proxy CONNECT status 502 or 503
The tunnel was refused before an HTTPS response existed. Use the CONNECT troubleshooting guide and compare a small echo request.
Destination HTTP status 403 or 429
Follow the destination’s access rules or rate limit. The transport has delivered an HTTP response; this is not automatically a proxy password problem.
TLS certificate verification failed
Check the target hostname, system clock and trusted certificate chain. Keep certificate verification enabled.
connection failed; check gateway, DNS and timeout settings
The request failed before any HTTP response, without a CONNECT status or certificate error to report. Check the gateway host, the HTTP port 7000 and DNS from the worker. The connection-refused guide separates a refused port, an unresolved name and a timeout.
Deadline or response-read failure
Inspect which stage stalled and whether the configured budget suits the workload. Keep retries bounded and use them only when replay is safe.

Verified locally, then test your own workflow

Thirteen cases passed through a local authenticated CONNECT proxy and HTTPS origin using synthetic credentials. They covered password punctuation, rejected credentials, a refused tunnel, destination refusal, redirect handling, invalid payloads, exact and excessive body sizes, certificate rejection and CONNECT, header and trickling-body timeouts. No proxy authentication header reached the destination; diagnostics contained no fixture credentials.

Endpoints and timeout durations were replaced for the fixture. It did not test production availability, country selection, SOCKS5 or HTTPS transport to the proxy. Reuse a client for a stable configuration in a service, bound your work queue separately from connections, and measure successful application results before scaling. A reused connection can also affect rotation observations: see the session guide and traffic planning.

What is not allowed

Use proxies for authorised testing and research within destination terms and the acceptable-use policy.

Go HTTP proxy setup with authentication · Portproof