Java gives you more proxy knobs than any other runtime here, and that is the problem: JVM-wide system properties, a java.net.Proxy object for the old HttpURLConnection, a ProxySelector for the modern HttpClient, and OkHttp on top of all of it. They do not behave the same, one of them defaults to silently blocking your proxy password, and the newest client cannot do SOCKS at all. This guide sorts them out: the property route, the modern HttpClient, the legacy Proxy object, OkHttp, SOCKS5, authentication (including the gotcha), rotation, and verifying your exit IP.
We run a proxy network and field a steady stream of JVM proxy tickets, so the mistakes below are ones we see weekly: an authenticated proxy that returns an endless 407 over HTTPS, a SOCKS proxy handed to a client that cannot speak it. Every example is copy-paste runnable on Java 11 or later. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints without a key.
How do you use a proxy with Java?
For a whole-process default, set the http.proxyHost, http.proxyPort, https.proxyHost, and https.proxyPort system properties. For a single client, build a java.net.http.HttpClient with .proxy(ProxySelector.of(new InetSocketAddress(host, port))). For SOCKS or for OkHttp, construct a java.net.Proxy. The proxy is either a JVM-global property or a per-client object, never a per-request field.
The system-property route
The quickest way to proxy an entire JVM is the networking system properties. They can be set on the command line with -D or at runtime with System.setProperty before the first connection:
System.setProperty("http.proxyHost", "203.0.113.7");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "203.0.113.7");
System.setProperty("https.proxyPort", "8080");
// http.nonProxyHosts default is "localhost|127.*|[::1]"
A few defaults from the JDK networking properties are worth knowing: http.proxyPort defaults to 80 and https.proxyPort to 443, so always set the port to your proxy's actual port. http.nonProxyHosts (default localhost|127.*|[::1]) is the bypass list, pipe-separated with * wildcards, and HTTPS reuses it rather than having its own. The catch with properties is that they are global and not every library honors them, so for anything beyond a quick default, prefer the per-client APIs below.
java.net.http.HttpClient (Java 11+)
The modern client takes a ProxySelector, and ProxySelector.of builds one for a single HTTP/HTTPS proxy:
import java.net.InetSocketAddress;
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 ProxyDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("203.0.113.7", 8080)))
.connectTimeout(Duration.ofSeconds(15))
.build();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://httpbin.org/ip")).build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
For an https:// target the client opens a CONNECT tunnel through the proxy and performs TLS end to end with the origin, so a plain HTTP proxy carries your encrypted traffic without seeing inside it.
Authenticating, and the Basic-over-tunnel trap
This is the part that costs people an afternoon. You supply proxy credentials with a java.net.Authenticator, returning them only for proxy challenges:
import java.net.Authenticator;
import java.net.PasswordAuthentication;
// The JVM disables Basic auth for HTTPS CONNECT tunnels by default.
// Clear that BEFORE building the client, or proxy auth on https:// targets never succeeds.
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("203.0.113.7", 8080)))
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
return new PasswordAuthentication("user", "pass".toCharArray());
}
return null; // not a proxy challenge, do not leak proxy creds
}
})
.build();
The load-bearing line is the System.setProperty call. The JDK's net.properties ships jdk.http.auth.tunneling.disabledSchemes=Basic, which disables the Basic scheme "when tunneling HTTPS over a proxy" with HTTP CONNECT. In plain terms: reach an https:// site through an authenticated proxy and Java refuses to answer the proxy's Basic challenge, so you get a 407 that never clears, no matter how correct your password is. Removing Basic from that list (an empty string clears it) restores normal proxy Basic auth. You can also set it with -Djdk.http.auth.tunneling.disabledSchemes= on the command line or edit conf/net.properties. The property exists because Basic sends the password in effectively cleartext, but for an authenticated proxy over a tunnel you control, clearing it is the intended fix.
java.net.Proxy for HttpURLConnection
Plenty of Java code still uses HttpURLConnection, and it takes a java.net.Proxy object directly. The constructor is new Proxy(Proxy.Type type, SocketAddress sa), with Proxy.Type being DIRECT, HTTP, or SOCKS:
import java.net.*;
import java.io.*;
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("203.0.113.7", 8080));
HttpURLConnection conn =
(HttpURLConnection) URI.create("https://httpbin.org/ip").toURL().openConnection(proxy);
conn.setConnectTimeout(5000);
conn.setReadTimeout(15000);
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
in.lines().forEach(System.out::println);
}
The same java.net.Proxy type is what you pass to OkHttp below, so it is worth being comfortable with it even if you never touch HttpURLConnection directly.
SOCKS5 proxies
Here is the sharp edge: the modern java.net.http.HttpClient has no SOCKS support, because ProxySelector.of only produces HTTP/HTTPS selectors. You have three real options for SOCKS.
The system-property route sets it at the socket layer, which HttpURLConnection and plain sockets respect:
System.setProperty("socksProxyHost", "198.51.100.14");
System.setProperty("socksProxyPort", "1080"); // default is 1080
System.setProperty("socksProxyVersion", "5"); // default is 5
System.setProperty("java.net.socks.username", "user"); // SOCKS5 auth
System.setProperty("java.net.socks.password", "pass");
Or construct a SOCKS Proxy object, which you can hand to a URLConnection or to OkHttp:
Proxy socks = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("198.51.100.14", 1080));
One DNS caveat specific to Java's Proxy.Type.SOCKS: when you build the InetSocketAddress with a hostname it may be resolved locally before the connection, so if you need the proxy to resolve names (the socks5h behavior, for privacy or for names that only resolve on the proxy's network), use InetSocketAddress.createUnresolved(host, port) for the destination so the name travels to the proxy. We cover why that split matters in what is a SOCKS5 proxy.
OkHttp
For anything serious, most Java HTTP code uses OkHttp, and it takes the same java.net.Proxy object plus a dedicated authenticator for proxy challenges. From OkHttp's own API, proxy(Proxy) sets the proxy and "takes precedence over proxySelector," while proxyAuthenticator(Authenticator) "responds to challenges from proxy servers" as distinct from authenticator, which handles origin servers:
import okhttp3.*;
import java.net.InetSocketAddress;
import java.net.Proxy;
OkHttpClient client = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("203.0.113.7", 8080)))
.proxyAuthenticator((route, response) -> {
if (response.request().header("Proxy-Authorization") != null) {
return null; // already tried these creds, give up instead of looping
}
String credential = Credentials.basic("user", "pass");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
})
.build();
Request req = new Request.Builder().url("https://httpbin.org/ip").build();
try (Response res = client.newCall(req).execute()) {
System.out.println(res.body().string());
}
Two OkHttp details save you grief. The Proxy-Authorization null-check prevents an infinite retry loop when the proxy keeps rejecting the same credentials, returning null tells OkHttp to stop trying. And for a SOCKS proxy you change nothing but the Proxy.Type: new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("198.51.100.14", 1080)) passed to .proxy(). OkHttp does not suffer the HttpClient SOCKS limitation.
Rotating across a pool
One IP sending every request is the exact shape a rate limiter watches for. A custom ProxySelector rotates cleanly, and the nice part is it plugs into both the modern HttpClient (.proxy(selector)) and OkHttp (.proxySelector(selector)), so you write it once:
import java.io.IOException;
import java.net.*;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
class RotatingProxySelector extends ProxySelector {
private final List<Proxy> pool;
RotatingProxySelector(List<Proxy> pool) {
this.pool = pool;
}
@Override
public List<Proxy> select(URI uri) {
// Called per request: hand back a different exit each time.
return List.of(pool.get(ThreadLocalRandom.current().nextInt(pool.size())));
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException e) {
// A dead exit lands here; the request simply fails and you retry.
}
}
Wire it into the modern client:
List<Proxy> pool = List.of(
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("203.0.113.7", 8080)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("203.0.113.24", 3128)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("198.51.100.14", 8080)));
HttpClient client = HttpClient.newBuilder()
.proxy(new RotatingProxySelector(pool))
.build();
You do not have to hardcode the list either. Our free proxy API returns a fresh pool as plain text you can pull at startup and map into Proxy objects:
String raw = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create(
"https://hproxy.com/api/proxy-list?format=txt&protocol=http&recent=true&limit=50")).build(),
HttpResponse.BodyHandlers.ofString()).body();
List<Proxy> pool = raw.lines()
.map(String::trim).filter(s -> !s.isEmpty())
.map(line -> {
String[] hp = line.split(":");
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(hp[0], Integer.parseInt(hp[1])));
})
.toList();
For production, a rotating gateway that gives you one endpoint and a fresh residential IP per request removes list management entirely, but rotating a real list first is the best way to understand what that gateway does for you.
Verify the exit IP
Never assume a proxy is working. Confirm the target sees the proxy's address and not yours, and do it before you trust an exit with real traffic:
import java.net.*;
import java.net.http.*;
HttpClient direct = HttpClient.newHttpClient();
HttpClient proxied = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("203.0.113.7", 8080)))
.build();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://httpbin.org/ip")).build();
String real = direct.send(req, HttpResponse.BodyHandlers.ofString()).body();
String viaProxy = proxied.send(req, HttpResponse.BodyHandlers.ofString()).body();
System.out.println("real: " + real);
System.out.println("proxy: " + viaProxy);
if (real.equals(viaProxy)) {
throw new IllegalStateException("proxy is not changing your IP");
}
If the two responses show the same IP, the request never went through the proxy, often because a system property was set too late or the host was in nonProxyHosts. It is also worth checking what headers arrive at https://httpbin.org/headers, because a transparent proxy can forward your real address in X-Forwarded-For even while origin looks changed. Our proxy checker runs that whole battery, exit IP, anonymity grade, real geolocation, and latency, in one paste if you would rather not script it.
Where to go from here
Two habits keep Java proxy code alive unattended: set a connectTimeout (and read timeout) so a silent proxy cannot hang a thread, and verify every exit before you trust it. And if you use authenticated proxies over HTTPS, remember to clear jdk.http.auth.tunneling.disabledSchemes once at startup. For a live pool to test against, the free proxy list re-verifies its entries every few minutes, and the free API hands them to you with no key.
From here, the cURL guide is the fastest way to sanity-check a proxy from the shell before you wire it into Java, the Python requests guide is a close sibling if your stack spans both, and proxies for web scraping covers choosing the right proxy type and the request hygiene no HTTP client can add for you. When a project graduates from experiments to production, get IPs nobody else is burning on our paid pools.
Sources
- JDK networking properties (Java SE 21):
http.proxyHost/http.proxyPort(default 80),https.proxyHost/https.proxyPort(default 443),http.nonProxyHosts(defaultlocalhost|127.*|[::1]),socksProxyHost/socksProxyPort(default 1080),socksProxyVersion(default 5), andjava.net.socks.username/password. - java.net.Proxy and ProxySelector (Java SE 21): the
Proxy(Proxy.Type, SocketAddress)constructor, theDIRECT/HTTP/SOCKStypes, andProxySelector.of(InetSocketAddress)returning a selector for HTTP/HTTPS. - OpenJDK net.properties:
jdk.http.auth.tunneling.disabledSchemes=Basicdisabling Basic auth for HTTPSCONNECTtunneling through a proxy. - OkHttpClient.Builder source (OkHttp 4.12.0):
proxy(Proxy?)taking precedence overproxySelector(ProxySelector), andproxyAuthenticator(Authenticator)responding to proxy challenges as opposed toauthenticatorfor origin servers.