{"id":804,"date":"2026-07-19T01:12:42","date_gmt":"2026-07-18T22:12:42","guid":{"rendered":"https:\/\/mexela.com\/blog\/go-http-proxy\/"},"modified":"2026-07-19T21:18:21","modified_gmt":"2026-07-19T18:18:21","slug":"go-http-proxy","status":"publish","type":"post","link":"https:\/\/mexela.com\/blog\/go-http-proxy\/","title":{"rendered":"Go HTTP Proxy Setup: Environment, Transport, Auth, and Testing"},"content":{"rendered":"<p class=\"mexela-answer\">To use a proxy with Go <code>net\/http<\/code>, either set <code>HTTP_PROXY<\/code>, <code>HTTPS_PROXY<\/code>, and <code>NO_PROXY<\/code> before the process starts and use <code>http.ProxyFromEnvironment<\/code>, or parse one explicit proxy URL and assign <code>http.ProxyURL(proxyURL)<\/code> to a cloned <code>http.Transport<\/code>. Reuse the transport and client, set bounded timeouts, close every response body, and verify the exit before sending application traffic.<\/p>\n<p class=\"mexela-scope\"><strong>Scope:<\/strong> this tutorial covers outbound HTTP and HTTPS requests made by Go clients. It does not build a proxy server or claim that all third-party Go SDKs use <code>http.DefaultClient<\/code>. Review protocol choices in <a href=\"\/blog\/http-https-socks5-proxies\/\">HTTP, HTTPS, and SOCKS5 proxies<\/a> and find adjacent client examples in the <a href=\"\/blog\/proxy-setup-developer-guides\/\">Proxy Setup and Developer Guides hub<\/a>.<\/p>\n<h2 id=\"project\">Create a small acceptance program<\/h2>\n<p>Use a maintained Go toolchain approved by the project. Start in an empty module so the first result reflects the standard library and the selected proxy, not an SDK with its own transport.<\/p>\n<pre><code>mkdir proxycheck\ncd proxycheck\ngo mod init example.com\/proxycheck\ngo version<\/code><\/pre>\n<p>Record the operating system and Go version. The public <a href=\"https:\/\/pkg.go.dev\/net\/http\" rel=\"noopener\"><code>net\/http<\/code> documentation<\/a> describes the default transport, environment proxy behavior, supported proxy schemes, connection reuse, and concurrency guarantees. Use that as the source of truth for the toolchain version in production.<\/p>\n<h2 id=\"proxyfromenvironment\"><code>ProxyFromEnvironment<\/code>: use process configuration intentionally<\/h2>\n<p>Go&#8217;s default transport uses <code>http.ProxyFromEnvironment<\/code>. It reads <code>HTTP_PROXY<\/code>, <code>HTTPS_PROXY<\/code>, and <code>NO_PROXY<\/code>, including lowercase forms. The proxy value can be a complete URL; if a compatible implementation accepts a bare <code>host:port<\/code>, an HTTP scheme is assumed, but an explicit URL is easier to review.<\/p>\n<pre><code># macOS or Linux\nexport HTTP_PROXY='http:\/\/HOST:PORT'\nexport HTTPS_PROXY='http:\/\/HOST:PORT'\nexport NO_PROXY='localhost,127.0.0.1,.internal.example'\ngo run .\n\n# Windows PowerShell\n$Env:HTTP_PROXY = 'http:\/\/HOST:PORT'\n$Env:HTTPS_PROXY = 'http:\/\/HOST:PORT'\n$Env:NO_PROXY = 'localhost,127.0.0.1,.internal.example'\ngo run .<\/code><\/pre>\n<p>Set environment values before the process begins and do not treat them as a per-request switch. The standard implementation caches environment-derived proxy behavior, and changing variables after requests start is an unreliable configuration strategy. For different routes, create explicit clients with separate transports.<\/p>\n<p>Be careful in CGI-like environments: Go protects against unsafe use of uppercase <code>HTTP_PROXY<\/code> when the <code>REQUEST_METHOD<\/code> environment variable indicates CGI. This behavior prevents an inbound <code>Proxy<\/code> header from becoming outbound proxy configuration. A server launched under CGI should use an explicit, reviewed transport instead of trying to defeat that protection.<\/p>\n<h2 id=\"clone-default-transport\">Clone the default transport instead of rebuilding it blindly<\/h2>\n<p>The default transport contains sensible dial, keep-alive, TLS-handshake, and idle-connection settings. Clone it, make the intended proxy and timeout changes, and reuse it:<\/p>\n<pre><code>package main\n\nimport (\n    \"fmt\"\n    \"net\/http\"\n    \"time\"\n)\n\nfunc main() {\n    transport := http.DefaultTransport.(*http.Transport).Clone()\n    transport.Proxy = http.ProxyFromEnvironment\n    transport.MaxIdleConns = 100\n    transport.MaxIdleConnsPerHost = 10\n    transport.IdleConnTimeout = 90 * time.Second\n\n    client := &amp;http.Client{\n        Transport: transport,\n        Timeout:   30 * time.Second,\n    }\n\n    response, err := client.Get(\"https:\/\/api.ipify.org?format=json\")\n    if err != nil {\n        panic(err)\n    }\n    defer response.Body.Close()\n\n    fmt.Println(response.Status)\n}<\/code><\/pre>\n<p>The type assertion is appropriate for the standard default transport in an application that controls process initialization. A library should accept an <code>http.RoundTripper<\/code> or <code>*http.Client<\/code> from its caller instead of assuming ownership of global defaults.<\/p>\n<h2 id=\"proxyurl\">Use <code>ProxyURL<\/code> for an explicit per-client route<\/h2>\n<p>For a route owned by one service component, parse the URL once and assign <code>http.ProxyURL<\/code>:<\/p>\n<pre><code>proxyURL, err := url.Parse(\"http:\/\/HOST:PORT\")\nif err != nil {\n    return fmt.Errorf(\"parse proxy URL: %w\", err)\n}\n\ntransport := http.DefaultTransport.(*http.Transport).Clone()\ntransport.Proxy = http.ProxyURL(proxyURL)\n\nclient := &amp;http.Client{\n    Transport: transport,\n    Timeout:   30 * time.Second,\n}<\/code><\/pre>\n<p>Validate configuration before parsing: allow only expected schemes, require a host and port, and reject fragments or destination-like paths. The scheme describes the connection to the proxy. An HTTP proxy can tunnel HTTPS destinations with CONNECT.<\/p>\n<p>An explicit transport ignores <code>NO_PROXY<\/code> unless the proxy function implements bypass logic. If bypass is required, either use <code>ProxyFromEnvironment<\/code> with a controlled process environment or write a small reviewed proxy-selection function that returns <code>nil, nil<\/code> only for exact approved hosts.<\/p>\n<h2 id=\"credentials\">Proxy credentials and URL user information<\/h2>\n<p>The <code>Transport.Proxy<\/code> documentation states that when the returned proxy URL contains user information, the transport sends username\/password in a <code>Proxy-Authorization<\/code> header. Construct the URL from a protected secret, do not hardcode it, and never log <code>proxyURL.String()<\/code> after adding credentials.<\/p>\n<pre><code>proxyURL, err := url.Parse(\"http:\/\/HOST:PORT\")\nif err != nil {\n    return fmt.Errorf(\"invalid proxy URL: %w\", err)\n}\n\ntransport := http.DefaultTransport.(*http.Transport).Clone()\ntransport.Proxy = http.ProxyURL(proxyURL)<\/code><\/pre>\n<p>If authentication is required, load <code>USER<\/code> and <code>PASSWORD<\/code> from the project secret provider and assign URL user information with <code>url.UserPassword<\/code> before installing the proxy function. User information does not make the secret safe to print. Load <code>USER<\/code> and <code>PASSWORD<\/code> from the project&#8217;s secret provider, limit their process scope, and rotate a password exposed in logs or panic output.<\/p>\n<p>A 407 belongs to proxy authentication. A 401 returned after the tunnel reaches a private API belongs to the destination. Keep proxy credentials separate from <code>Authorization<\/code> headers on the request. The <a href=\"\/blog\/proxy-authentication-username-password-vs-ip-auth\/\">authentication guide<\/a> compares this model with source-IP allowlisting.<\/p>\n<h2 id=\"no-proxy\"><code>NO_PROXY<\/code>: make bypass behavior explicit<\/h2>\n<p>The official <a href=\"https:\/\/pkg.go.dev\/golang.org\/x\/net\/http\/httpproxy\" rel=\"noopener\"><code>httpproxy<\/code> documentation<\/a> describes comma-separated bypass entries, including IP prefixes, CIDR notation, domain names, subdomain forms, and optional ports. Loopback addresses are bypassed as a special case. Exact matching details matter, so test the host strings the application actually requests.<\/p>\n<p>A narrow example is <code>NO_PROXY=localhost,127.0.0.1,.internal.example<\/code>. The leading dot is a deliberate subdomain rule. Do not use a broad corporate suffix if only one service must bypass. Every bypass changes where traffic exits and what evidence a regional test can claim.<\/p>\n<p>Log whether a destination was selected for proxy or direct routing, but do not log the credential-bearing URL. You can call the proxy function with a sanitized request in a unit test and assert whether the returned URL is nil.<\/p>\n<h2 id=\"transport-reuse\">Transport reuse, bodies, and connection pooling<\/h2>\n<p>The <code>net\/http<\/code> package documentation says clients and transports are safe for concurrent use and should be reused. Creating a transport per request prevents effective pooling and can exhaust ephemeral ports. Keep one client per stable route and policy.<\/p>\n<p>Close every response body. When the application needs connection reuse, read or discard the remaining body according to its protocol and size policy before closing. Do not read an unbounded error page merely to save a connection.<\/p>\n<pre><code>response, err := client.Do(request)\nif err != nil {\n    return fmt.Errorf(\"proxy request: %w\", err)\n}\ndefer response.Body.Close()\n\nbody, err := io.ReadAll(io.LimitReader(response.Body, 1&lt;&lt;20))\nif err != nil {\n    return fmt.Errorf(\"read bounded response: %w\", err)\n}<\/code><\/pre>\n<p>Use <code>MaxConnsPerHost<\/code> or application-level semaphores to enforce approved concurrency. A large proxy pool does not change the aggregate load placed on one destination.<\/p>\n<h2 id=\"timeouts\">Use layered timeouts and cancellation<\/h2>\n<p><code>http.Client.Timeout<\/code> bounds the complete exchange. The transport can also bound proxy\/gateway connection, TLS handshake, response headers, and idle connections. A request context lets the caller cancel when the job is no longer needed.<\/p>\n<pre><code>dialer := &amp;net.Dialer{\n    Timeout:   10 * time.Second,\n    KeepAlive: 30 * time.Second,\n}\n\ntransport := http.DefaultTransport.(*http.Transport).Clone()\ntransport.Proxy = http.ProxyFromEnvironment\ntransport.DialContext = dialer.DialContext\ntransport.TLSHandshakeTimeout = 10 * time.Second\ntransport.ResponseHeaderTimeout = 20 * time.Second\n\nclient := &amp;http.Client{Transport: transport, Timeout: 30 * time.Second}<\/code><\/pre>\n<p>Do not retry every error. DNS failures, 407, certificate failures, 403, and many application responses require correction or review, not more traffic. Retry only operations known to be safe and idempotent, with a small limit, backoff, jitter, and respect for <code>Retry-After<\/code>.<\/p>\n<h2 id=\"exit-verification\">Exit verification with bounded JSON parsing<\/h2>\n<p>Use one neutral HTTPS endpoint, require a 2xx response, check the content type when available, and decode a small JSON object:<\/p>\n<pre><code>type ipResult struct {\n    IP string `json:\"ip\"`\n}\n\nresponse, err := client.Get(\"https:\/\/api.ipify.org?format=json\")\nif err != nil {\n    return fmt.Errorf(\"exit check request: %w\", err)\n}\ndefer response.Body.Close()\n\nif response.StatusCode &lt; 200 || response.StatusCode &gt;= 300 {\n    return fmt.Errorf(\"exit check status: %s\", response.Status)\n}\n\nvar result ipResult\ndecoder := json.NewDecoder(io.LimitReader(response.Body, 4096))\nif err := decoder.Decode(&amp;result); err != nil {\n    return fmt.Errorf(\"decode exit check: %w\", err)\n}\nif net.ParseIP(result.IP) == nil {\n    return fmt.Errorf(\"exit check returned invalid IP\")\n}\nfmt.Println(map[string]string{\"exit_ip\": result.IP})<\/code><\/pre>\n<p class=\"mexela-expected\"><strong>Expected observation:<\/strong> the request succeeds with TLS validation enabled, returns a valid public address within the size\/time limits, and the address matches the assigned proxy exit rather than the intentionally direct baseline.<\/p>\n<h2 id=\"connect-troubleshooting\">CONNECT troubleshooting and proxy responses<\/h2>\n<p>For an HTTPS destination over an HTTP proxy, the transport asks the proxy to establish a CONNECT tunnel. A 407 during CONNECT means the gateway rejected authentication. A 502 or timeout can mean the gateway could not reach the destination, the destination refused the route, or an intermediate network failed.<\/p>\n<p>Current Go exposes <code>OnProxyConnectResponse<\/code> on <code>http.Transport<\/code> for inspecting a proxy response before the standard success check. Use it only for sanitized status\/header diagnostics; never record <code>Proxy-Authorization<\/code> or arbitrary bodies.<\/p>\n<pre><code>transport.OnProxyConnectResponse = func(\n    ctx context.Context,\n    proxyURL *url.URL,\n    connectRequest *http.Request,\n    connectResponse *http.Response,\n) error {\n    slog.InfoContext(ctx, \"proxy connect\",\n        \"proxy_host\", proxyURL.Host,\n        \"target_host\", connectRequest.Host,\n        \"status\", connectResponse.StatusCode)\n    return nil\n}<\/code><\/pre>\n<p>Confirm the exact Go version supports the field before using it. The core client should still return failures normally; diagnostics must not convert a rejected CONNECT response into success.<\/p>\n<h2 id=\"troubleshooting\">Go proxy troubleshooting by symptom<\/h2>\n<table>\n<thead>\n<tr>\n<th>Signal<\/th>\n<th>Likely boundary<\/th>\n<th>First check<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>proxyconnect tcp<\/code> name\/route error<\/td>\n<td>Gateway DNS, address, port, firewall<\/td>\n<td>Validate the configured proxy URL and sanitized host<\/td>\n<\/tr>\n<tr>\n<td>407 during CONNECT<\/td>\n<td>Proxy authentication<\/td>\n<td>Check URL user info, account state, or allowlisted public IP<\/td>\n<\/tr>\n<tr>\n<td><code>x509<\/code> error<\/td>\n<td>TLS hostname, clock, trust chain, or interception<\/td>\n<td>Inspect certificate evidence; do not use insecure skip verify<\/td>\n<\/tr>\n<tr>\n<td>Request unexpectedly direct<\/td>\n<td><code>NO_PROXY<\/code>, nil proxy function, or SDK transport<\/td>\n<td>Inspect the exact client and proxy decision for the host<\/td>\n<\/tr>\n<tr>\n<td>Environment change ignored<\/td>\n<td>Cached environment configuration<\/td>\n<td>Set variables before process start or create an explicit client<\/td>\n<\/tr>\n<tr>\n<td>Too many open connections<\/td>\n<td>Transport\/body lifecycle<\/td>\n<td>Reuse transport and close bounded bodies<\/td>\n<\/tr>\n<tr>\n<td>Only one destination returns 403\/429<\/td>\n<td>Destination policy or rate limit<\/td>\n<td>Stop, preserve status, and review the approved workflow<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Follow <a href=\"\/blog\/test-if-your-proxy-is-working\/\">the layered proxy test<\/a> and <a href=\"\/blog\/common-proxy-errors-fixes\/\">common proxy error guide<\/a> before cycling endpoints. Record Go version, sanitized gateway, destination host, stage, status, and duration without logging secrets or sensitive response content.<\/p>\n<h2 id=\"library-design\">Pass clients into libraries instead of changing globals<\/h2>\n<p>A reusable package should accept <code>*http.Client<\/code> or an interface implemented by it. The application owns proxy selection, timeouts, authentication, and connection lifetime. This keeps tests deterministic and prevents one package from changing global transport for unrelated requests.<\/p>\n<p>Create separate named clients for direct internal traffic and proxied external traffic when both are required. Make the choice at a clear call boundary. Do not mutate one transport while concurrent requests are running.<\/p>\n<p class=\"mexela-limits\"><strong>Responsible-use boundary:<\/strong> send only authorized requests, follow destination terms and aggregate rate limits, bound concurrency and retries, prefer official APIs where required, and stop on explicit denial. A proxy route remains accountable application traffic.<\/p>\n<h2 id=\"next-step\">Select a stable Go route after the client is observable<\/h2>\n<p>Record protocol, country, authentication model, exact bypasses, concurrent requests, client lifetime, and measured connect\/response times. If the approved service needs an exclusive stable exit, compare those requirements with current <a href=\"\/private-proxies\/\">private proxy options<\/a> only after the direct baseline, proxied exit check, application smoke test, and cleanup behavior are repeatable.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Configure Go net\/http with ProxyFromEnvironment or an explicit ProxyURL, handle credentials, apply NO_PROXY, reuse transports, verify the exit, and diagnose CONNECT failures.<\/p>\n","protected":false},"author":0,"featured_media":805,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[189],"tags":[],"_links":{"self":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/804"}],"collection":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/comments?post=804"}],"version-history":[{"count":1,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/804\/revisions"}],"predecessor-version":[{"id":809,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/posts\/804\/revisions\/809"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media\/805"}],"wp:attachment":[{"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/media?parent=804"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/categories?post=804"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mexela.com\/blog\/wp-json\/wp\/v2\/tags?post=804"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}