Skip to content
ProxyForge

Go HTTP proxy and Java HttpClient proxy setup

ProxyForge engineeringUpdated 7 min read

A Go HTTP proxy is configured on the transport: set Proxy: http.ProxyURL(u) on an http.Transport, where u is parsed from a URL such as http://USERNAME:[email protected]:PORT, and Go sends the credentials in the URL as Proxy-Authorization, including on the CONNECT request that tunnels HTTPS. In Java 11 and later, HttpClient takes ProxySelector.of(address) plus an Authenticator for the credentials. Java has one trap that Go does not: by default the JDK refuses to send Basic credentials when tunneling HTTPS, so you must clear jdk.http.auth.tunneling.disabledSchemes.

Every example here was compiled and run against a local authenticating HTTP proxy and a SOCKS5 proxy: the Go code with Go 1.27.1 and golang.org/x/net v0.59.0, after go vet; the Java code with Temurin JDK 25.0.4 and OkHttp 5.5.0. Examples read the connection details from environment variables. Your dashboard generates the exact connection string for the country and session mode you choose.

Go: http.Transport with ProxyURL

Build one transport and one client for the Go HTTP proxy connection, and reuse them for the life of the process:

package main

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

func newProxyClient(proxyURL *url.URL) *http.Client {
	transport := &http.Transport{
		Proxy: http.ProxyURL(proxyURL),
		DialContext: (&net.Dialer{
			Timeout:   10 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		TLSHandshakeTimeout:   10 * time.Second,
		ResponseHeaderTimeout: 30 * time.Second,
		IdleConnTimeout:       90 * time.Second,
		MaxIdleConns:          100,
		MaxIdleConnsPerHost:   10,
		ForceAttemptHTTP2:     true,
	}
	return &http.Client{Transport: transport, Timeout: 60 * time.Second}
}

func main() {
	proxyURL, err := url.Parse(os.Getenv("PROXY_URL"))
	if err != nil {
		log.Fatalf("parse PROXY_URL: %v", err)
	}
	client := newProxyClient(proxyURL)

	resp, err := client.Get("https://api.ipify.org?format=json")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(body))
}

Go takes the username and password from the URL's userinfo and sends them preemptively: our test proxy saw Proxy-Authorization on the first CONNECT, with no 407 round trip first. It does the same for plain http:// targets, where the request goes to the proxy directly instead of through a tunnel. When the proxy rejects the credentials, client.Get returns an error that reads Proxy Authentication Required, not a response with status 407.

url.Parse decodes percent-encoding, so a password containing @ or : must be written as %40 or %3A in PROXY_URL. If the credentials arrive as separate values, build the URL instead and let Go handle the encoding:

proxyURL := &url.URL{
	Scheme: "http",
	User:   url.UserPassword(os.Getenv("PROXY_USERNAME"), os.Getenv("PROXY_PASSWORD")),
	Host:   os.Getenv("PROXY_HOST_PORT"),
}

Transport.ProxyConnectHeader adds headers to the CONNECT request only, if your gateway expects anything beyond the credentials.

Proxy settings from the environment

http.ProxyFromEnvironment reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY, or their lowercase forms, and picks the proxy by the request's scheme. http.DefaultTransport already uses it, so any program that uses the default client honors those variables. For a tuned transport, clone the default rather than starting from zero:

transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = http.ProxyFromEnvironment
client := &http.Client{Transport: transport, Timeout: 30 * time.Second}

Two behaviors to know. Requests to localhost and loopback addresses are never proxied, regardless of NO_PROXY; we confirmed that http://127.0.0.1 and http://localhost went direct while other hosts went through the proxy. And the environment is read once, on first use, so changing the variables inside a running process has no effect. Use http.ProxyURL with an explicit value when the proxy must be chosen at run time.

SOCKS5 in Go

net/http supports SOCKS5 itself: pass a socks5:// URL to http.ProxyURL and the transport dials through it, with credentials from the URL. The Go documentation states that socks5 is treated the same as socks5h, so the target hostname is sent to the proxy and resolved at the exit. Our test proxy received domain names for both schemes.

For SOCKS below the HTTP layer, such as a database driver or a raw TCP connection, golang.org/x/net/proxy provides a dialer:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"time"

	"golang.org/x/net/proxy"
)

func main() {
	auth := &proxy.Auth{
		User:     os.Getenv("PROXY_USERNAME"),
		Password: os.Getenv("PROXY_PASSWORD"),
	}
	base := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
	dialer, err := proxy.SOCKS5("tcp", os.Getenv("SOCKS_PROXY_ADDR"), auth, base)
	if err != nil {
		log.Fatal(err)
	}
	contextDialer, ok := dialer.(proxy.ContextDialer)
	if !ok {
		log.Fatal("SOCKS5 dialer does not support DialContext")
	}

	transport := &http.Transport{
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			return contextDialer.DialContext(ctx, network, addr)
		},
		TLSHandshakeTimeout: 10 * time.Second,
		IdleConnTimeout:     90 * time.Second,
	}
	client := &http.Client{Transport: transport, Timeout: 30 * time.Second}

	resp, err := client.Get("https://api.ipify.org?format=json")
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(body))
}

SOCKS_PROXY_ADDR is host:port, without a scheme. The dialer also passes the hostname to the proxy. For HTTP clients, the built-in socks5:// support is simpler and needs no dependency; SOCKS5 vs HTTP proxy covers when SOCKS is worth using at all.

Go timeouts, connection reuse and rotation

The zero-value http.Client has no timeout, and a request through a Go HTTP proxy that stops responding will wait forever. The transport above sets a limit on each phase, and Client.Timeout bounds the whole request including reading the body.

Setting What it bounds
net.Dialer.Timeout The TCP connection to the proxy.
TLSHandshakeTimeout The TLS handshake with the target, through the tunnel.
ResponseHeaderTimeout Waiting for response headers after the request is sent.
IdleConnTimeout How long an unused pooled connection is kept.
Client.Timeout The whole request, including reading the body.

Connection reuse only happens if you read the response body to the end and close it. Skip either and every request opens a new connection, which through a proxy means a new CONNECT and a new TLS handshake each time. MaxIdleConnsPerHost defaults to 2, which is low for concurrent work against one target; raise it to match your concurrency.

Reuse also decides your exit IP. A pooled connection to an HTTPS site is a tunnel, and most gateways fix the exit when the tunnel opens, so requests on one pooled connection share an address even on an endpoint set to rotate per request. With HTTP/2, many concurrent requests share a single connection. That suits a sticky session. When every request should get a fresh exit, turn reuse off:

transport := &http.Transport{
	Proxy:             http.ProxyURL(proxyURL),
	DisableKeepAlives: true,
}

With this setting our test proxy saw one CONNECT per request. Rotating vs sticky proxies covers choosing between the two modes.

Java 11+ HttpClient with ProxySelector and Authenticator

java.net.http.HttpClient takes a ProxySelector for the address and an Authenticator for the credentials:

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class ProxiedHttp {
    public static void main(String[] args) throws Exception {
        System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");

        String host = System.getenv("PROXY_HOST");
        int port = Integer.parseInt(System.getenv("PROXY_PORT"));
        String username = System.getenv("PROXY_USERNAME");
        char[] password = System.getenv("PROXY_PASSWORD").toCharArray();

        Authenticator proxyAuthenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                if (getRequestorType() != RequestorType.PROXY) {
                    return null;
                }
                return new PasswordAuthentication(username, password);
            }
        };

        HttpClient client = HttpClient.newBuilder()
                .proxy(ProxySelector.of(new InetSocketAddress(host, port)))
                .authenticator(proxyAuthenticator)
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.ipify.org?format=json"))
                .timeout(Duration.ofSeconds(30))
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() == 407) {
            throw new IllegalStateException("Proxy rejected the credentials (407)");
        }
        System.out.println(response.statusCode() + " " + response.body());
    }
}

Checking getRequestorType() keeps the proxy password from being offered to a target server that asks for authentication. HttpClient does not send credentials preemptively: each new connection makes a CONNECT without them, receives the 407 challenge, asks the Authenticator and retries. Our test proxy logged both attempts for every new connection, so reuse connections where you can.

connectTimeout bounds the connection to the proxy, and the per-request timeout bounds the wait for the response. HttpClient pools connections and is safe to share across threads, so create it once.

The Basic auth trap: jdk.http.auth.tunneling.disabledSchemes

Remove the System.setProperty line and the same program fails. The JDK's net.properties ships with:

jdk.http.auth.tunneling.disabledSchemes=Basic

That disables Basic authentication for HTTPS tunneling, a default introduced in Java 8u111. Our test proxy logged a single CONNECT with no credentials, and client.send returned a response with status 407 and a null body; the Authenticator was never consulted. Plain http:// targets worked, because the companion property for non-tunneled requests, jdk.http.auth.proxying.disabledSchemes, is empty by default. So a Java HttpClient proxy that works for http:// and fails for https:// is almost always this property.

Three ways to clear it:

  1. On the command line, which applies before any code runs: java -Djdk.http.auth.tunneling.disabledSchemes= -jar app.jar.
  2. In code, as the first statement in main, before any HttpClient or HttpURLConnection is created.
  3. In $JAVA_HOME/conf/net.properties, which affects every application on that JDK and is easy to lose on an upgrade. Prefer the first two.

The default exists for a reason: Basic credentials on a CONNECT request travel to the proxy unencrypted. If your security review objects to that, IP allowlisting removes the credentials from the request entirely, and the JDK has nothing to disable.

When the credentials are wrong, the JDK asks the Authenticator again and retries a few times. On JDK 25 the failure then surfaced as an IOException wrapping a NullPointerException, not as a 407 response, so log the full exception chain rather than matching on a status code alone.

SOCKS5 and Java HttpClient

java.net.http.HttpClient supports only HTTP proxies. We gave it a ProxySelector that returned a SOCKS proxy: the request succeeded, and our SOCKS proxy logged nothing. The client had gone direct without raising an error. Use the HTTP endpoint from Java, which every ProxyForge line provides, and verify the exit IP at start-up whichever client you use.

OkHttp: proxy and proxyAuthenticator

OkHttp is not subject to the JDK property. It takes a java.net.Proxy and a proxyAuthenticator:

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.Duration;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class OkProxy {
    public static void main(String[] args) throws Exception {
        String host = System.getenv("PROXY_HOST");
        int port = Integer.parseInt(System.getenv("PROXY_PORT"));
        String credential = Credentials.basic(System.getenv("PROXY_USERNAME"), System.getenv("PROXY_PASSWORD"));

        OkHttpClient client = new OkHttpClient.Builder()
                .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)))
                .proxyAuthenticator((route, response) -> {
                    if (credential.equals(response.request().header("Proxy-Authorization"))) {
                        return null;
                    }
                    return response.request().newBuilder()
                            .header("Proxy-Authorization", credential)
                            .build();
                })
                .connectTimeout(Duration.ofSeconds(10))
                .callTimeout(Duration.ofSeconds(60))
                .build();

        Request request = new Request.Builder().url("https://api.ipify.org?format=json").build();
        try (Response response = client.newCall(request).execute()) {
            System.out.println(response.code() + " " + response.body().string());
        }
    }
}

OkHttp calls the proxyAuthenticator before the first CONNECT as well as after a 407, so our test proxy received the credentials on the first attempt. The check at the top of the lambda returns null when the credentials were already sent and rejected, which ends the attempt with IOException: Failed to authenticate with proxy instead of retrying with the same values. OkHttp also accepts Proxy.Type.SOCKS; it then takes SOCKS credentials from the JVM-wide java.net.Authenticator.setDefault, not from proxyAuthenticator.

Checking the setup before production

A misconfigured Go HTTP proxy or Java client rarely fails loudly; Java's SOCKS case above succeeds from the wrong address. At start-up, request an IP echo service such as https://api.ipify.org through the configured client, compare the result with the server's own public address, and refuse to start if they match. Log which proxy host the process is using, never the credentials.

Keep the credentials out of the code and out of images: load them from your secrets manager into the environment at start-up, as the proxy credentials guide describes. For the Node.js and Python equivalents of everything above, see the Node.js proxy guide and the Python Requests proxy guide.

Using ProxyForge from Go and Java

Every ProxyForge line accepts HTTP, HTTPS and SOCKS5 with username and password or IP allowlist authentication, so the Go examples work unchanged across lines, and the Java examples work over the HTTP endpoint. For Java services where clearing the JDK property is not acceptable, allowlist the service's egress IPs in the dashboard and drop the Authenticator.

For long-lived backend services that need a stable address, ISP proxies are static and dedicated to one customer; for sites that score IP reputation, the residential line rotates per request or holds a session for up to 60 minutes. If you are moving a service from another provider, the migration process mirrors your current endpoint and auth format so the code does not change while you compare.

FAQ

Related questions

Why does Java HttpClient return 407 even though my Authenticator supplies the proxy password?

Since Java 8u111 the JDK disables Basic authentication for HTTPS tunneling through a proxy, via the jdk.http.auth.tunneling.disabledSchemes property. Set that property to an empty string before the first HttpClient is created, or use IP allowlisting instead of a password.

Does Go's http.ProxyFromEnvironment work for requests to localhost?

No. Requests to localhost and loopback addresses are never proxied by ProxyFromEnvironment, whatever NO_PROXY says, which surprises people testing against a local server.

Can Java's built-in HttpClient use a SOCKS5 proxy?

No. java.net.http.HttpClient only supports HTTP proxies, and in our tests a ProxySelector returning a SOCKS proxy caused the request to go direct without an error. Use the HTTP proxy endpoint, or a client such as OkHttp that supports SOCKS.

How do I get a new proxy IP for every request in Go?

Point the transport at a rotating endpoint and set DisableKeepAlives to true, so each request opens its own tunnel. With keep-alive on, requests reuse the tunnel and usually share an exit.

Run it on a network you can account for

Order from 1 GB or 1 IP with no monthly minimum, or talk to an engineer about your workload first.

One business day, from a named engineer.