# Go HTTP Proxy: Configure net/http with Safe Auth and Bounded Requests

[All guides](https://proxylane.dev/blog) 

DEVELOPER GUIDES · 6 MIN READ

Route Go net/http through an explicit proxy, keep credentials out of logs, cap response work, and separate proxy failures from target responses.

**Founder, ProxyLane** Published September 18, 2026

On this page [Use an explicit http.Transport](https://proxylane.dev/blog/go-http-proxy#use-an-explicit-httptransport)  [Accept only a useful result](https://proxylane.dev/blog/go-http-proxy#accept-only-a-useful-result)

**Residential proxies from $2.50/GB**

Non-expiring traffic, location targeting and rotating or sticky sessions for your existing tools.

 [Sign up](https://proxylane.dev/register?interest=proxies)

Go's `net/http` already knows how to send an HTTP or HTTPS request through a proxy. The part that needs design is the boundary around it: which proxy is selected, where credentials live, how much time and body data one request may consume, and what counts as an accepted result.

 

The example below makes a bounded, idempotent `GET` to `https://example.com/`. Replace the target and marker with an endpoint you are allowed to access and a field your worker must accept. It reports only a status and body size. The proxy URL and password stay in memory and never enter a log or command argument.

 

## Use an explicit `http.Transport`

 

`http.ProxyURL` creates a proxy function for a fixed URL. The transport uses that function for HTTP requests and for an HTTPS target reached through CONNECT. Go also supports proxy selection from `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` through `ProxyFromEnvironment`, but an explicit URL makes this one request's route visible in code.

 

The baseline uses only the standard library and assumes Go 1.23 or later. It expects these environment bindings:

 

```text
PROXY_URL=http://proxy.example:3128
PROXY_USER=account-name             # optional
PROXY_PASSWORD=bound-secret         # optional
```

 

Keep the URL scheme, host, and port in `PROXY_URL`; provide credentials through the secret store or process environment. Do not put `user:password@` in a shared configuration value.

 

Save this as `main.go`:

 

```go
package main

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

const maxBodyBytes = int64(1 << 20)

func requiredEnv(name string) string {
	value := strings.TrimSpace(os.Getenv(name))
	if value == "" {
		panic("missing required environment binding: " + name)
	}
	return value
}

func readBody(response *http.Response) (string, error) {
	defer response.Body.Close()

	body, err := io.ReadAll(io.LimitReader(response.Body, maxBodyBytes+1))
	if err != nil {
		return "", err
	}
	if int64(len(body)) > maxBodyBytes {
		return "", fmt.Errorf("response body exceeded local cap")
	}
	return string(body), nil
}

func main() {
	proxyURL, err := url.Parse(requiredEnv("PROXY_URL"))
	if err != nil || proxyURL.Host == "" || proxyURL.User != nil || (proxyURL.Scheme != "http" && proxyURL.Scheme != "https") {
		panic("PROXY_URL must use an HTTP or HTTPS scheme, host, and no credentials")
	}

	proxyUser, hasUser := os.LookupEnv("PROXY_USER")
	proxyPassword, hasPassword := os.LookupEnv("PROXY_PASSWORD")
	if hasUser != hasPassword {
		panic("PROXY_USER and PROXY_PASSWORD must be supplied together")
	}
	if hasUser {
		proxyURL.User = url.UserPassword(proxyUser, proxyPassword)
	}

	transport := &http.Transport{
		Proxy:                 http.ProxyURL(proxyURL),
		DialContext:           (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
		TLSHandshakeTimeout:   10 * time.Second,
		ResponseHeaderTimeout: 10 * time.Second,
		MaxIdleConns:          8,
		MaxIdleConnsPerHost:   4,
	}
	defer transport.CloseIdleConnections()

	client := &http.Client{
		Transport: transport,
		Timeout:   15 * time.Second,
		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	request, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodGet,
		"https://example.com/",
		nil,
	)
	if err != nil {
		panic("request configuration failed")
	}

	response, err := client.Do(request)
	if err != nil {
		panic("request failed in transport or proxy negotiation")
	}

	status := response.StatusCode
	if status == http.StatusProxyAuthRequired {
		response.Body.Close()
		panic("proxy authentication or CONNECT negotiation returned HTTP 407")
	}
	body, bodyErr := readBody(response)
	if bodyErr != nil {
		panic("response body failed local acceptance checks")
	}
	if status != http.StatusOK {
		panic("target returned a non-accepted HTTP status")
	}
	if !strings.Contains(body, "Example Domain") {
		panic("target marker was absent")
	}

	fmt.Printf("status=%d body_bytes=%d\n", status, len(body))
}
```

 

The `Client.Timeout` covers connection, redirects, and body reading. The transport timeouts give the failure a more useful phase, while `CloseIdleConnections` releases pooled sockets when this short-lived program exits. In a worker, reuse one configured client and transport for a bounded unit of work instead of creating a transport per request.

 

This core example uses one attempt. If a worker adds retries for an idempotent `GET`, classify the failure first, honor a valid `Retry-After` value, and keep a job-level deadline. Transport, TLS, configuration, and proxy-authentication errors should stop immediately because repeating them does not repair the route. A 407 is returned by the proxy authentication boundary, so changing the secret or endpoint is the next action. Do not copy a retry policy to a POST or another operation with side effects.

 

## Accept only a useful result

 

The example accepts one result only when all three checks pass:

 

```text
input:  permitted target URL + expected marker + explicit proxy binding
output: status=200, marker present, body <= 1 MiB
```

 

`status=200` is an HTTP result, not proof that the record your application needs is present. The marker is intentionally simple; replace it with the field, schema, or checksum that makes the result useful to your job. The body cap is an application bound, not a proxy-account quota.

 

| Observation | Boundary | Next decision |
| --- | --- | --- |
| `PROXY_URL` rejected before a request | Local configuration | Check scheme, host, port, and secret binding without printing values |
| Transport error or CONNECT failure | Client to proxy or tunnel | Check reachability, proxy protocol, TLS, and proxy credentials; stop before retrying |
| HTTP 407 | Proxy authentication | Repair proxy auth or entitlement; do not treat it as a target response or keep retrying the same secret |
| 401 or 403 after a successful tunnel | Target policy | Check target credentials, permission, or target-side rules |
| 200 with no marker | Application validation | Reject the record and inspect the target response or parser |
| Body exceeds 1 MiB | Local resource policy | Reject or stream with a job-specific limit; do not silently raise the cap |

 

For a wider protocol comparison, see  [HTTP versus SOCKS5 proxies](https://proxylane.dev/blog/http-vs-socks5-proxies) . The  [curl proxy guide](https://proxylane.dev/blog/curl-proxy)  is useful for a hop-by-hop terminal probe, and  [HTTP 407 troubleshooting](https://proxylane.dev/blog/proxy-error-407)  explains why a CONNECT challenge can surface as a Go transport error instead of a response object.

 

ProxyLane's public residential offer starts from **$2.50/GB** with HTTP and SOCKS5 access, country, city, ISP, rotating and sticky options, and traffic that does not expire. Create a free account, then choose a traffic package separately; no payment is required at signup. Those are route and commercial inputs, not a Go benchmark or a target acceptance promise. When the target and acceptance rule are authorized,  [Sign up](https://proxylane.dev/register?interest=proxies) .

 

## Sources and further reading

- [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)

- [https://pkg.go.dev/net/http#Transport](https://pkg.go.dev/net/http#Transport)

- [https://pkg.go.dev/net/http#Client](https://pkg.go.dev/net/http#Client)

- [https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.8](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.8)

[Sign up](https://proxylane.dev/register?interest=proxies)

## Keep reading

[Use cases · 3 min read

### Amazon Scraper API vs Proxy: Choose a Product Data Source

Distinguish authorized Amazon APIs, licensed product data and proxy-based page checks by record quality and access rights.

 Read guide](https://proxylane.dev/blog/amazon-scraper-api-vs-proxy)   [Integrations · 5 min read

### Apify Custom Proxy Setup: Use Your Own Residential Route

Connect a buyer-owned proxy to an Apify Actor, keep the session boundary clear, and validate records instead of counting requests.

 Read guide](https://proxylane.dev/blog/apify-custom-proxy)   [Proxy fundamentals · 6 min read

### Australia Residential Proxies: Verify the AU Exit and State-Level Result

Separate Australian egress from en-AU content, AUD pricing, GST display, postcode validation and the state delivery context.

 Read guide](https://proxylane.dev/blog/australia-residential-proxies)

Canonical source: https://proxylane.dev/blog/go-http-proxy

Documentation index: https://proxylane.dev/llms.txt
