Java can send every request through a proxy, but three defaults make a setup look finished when it is not. Setting http.proxyHost alone sends every https:// request straight out. A proxy login over HTTPS fails with a 407 whatever the password. And the modern HttpClient quietly skips a SOCKS proxy and connects from your own address.
We tested 33 proxy setups on Java 17 and Java 8 against our own logging proxies, twice, with the same result every time. This guide shows what works, what fails, and the exact line that fixes each trap.
The short answer
- For the whole JVM, set both pairs of properties:
http.proxyHostwithhttp.proxyPort, andhttps.proxyHostwithhttps.proxyPort. - For one
HttpClient, passProxySelector.of(new InetSocketAddress(host, port)). - For a proxy login over HTTPS, clear
jdk.http.auth.tunneling.disabledSchemesas the first line ofmain, then add anAuthenticator. - For SOCKS5, use
HttpURLConnectionor OkHttp, neverHttpClient, and answer the login for the protocolSOCKS5.
| Setup | What happened in our test |
|---|---|
-Dhttp.proxyHost only, an https:// page | Went straight to the site |
-Dhttps.proxyHost, an https:// page | Went through the proxy |
HTTPS_PROXY environment variable | Ignored, went straight to the site |
Login proxy, https://, default settings | 407, Java never sent the password |
The same, disabledSchemes cleared | 200 through the proxy |
HttpClient given a SOCKS proxy | Ignored, went straight to the site |
java.net.socks.username set | Ignored, the SOCKS5 login failed |
An Authenticator answering SOCKS5 | Logged in, 200 through the proxy |

The code below leaves out its imports. Everything comes from java.io, java.net, java.net.http, java.time, java.util, java.util.concurrent.atomic and java.util.stream. We compiled every plain Java example on this page for Java 11 before publishing it.
Set a proxy for the whole JVM
The quickest route is a pair of system properties for each scheme, on the command line or in code before the first request:
java -Dhttp.proxyHost=203.0.113.7 -Dhttp.proxyPort=8080 -Dhttps.proxyHost=203.0.113.7 -Dhttps.proxyPort=8080 -jar app.jar
Set both pairs. In our test, http.proxyHost alone carried http:// pages through the proxy and sent an https:// page straight to the site. Nothing warns you, because the request succeeds either way, only from your own address. The pair people forget, https., is the one that carries every encrypted page.
Four more rules, from the same run and from the JDK networking properties:
- Environment variables do nothing. With
HTTPS_PROXYandHTTP_PROXYset, both Java clients went straight to the site. The JDK does not read them. - Local addresses skip the proxy.
http.nonProxyHostsdefaults tolocalhost|127.*|[::1], and a page on localhost went direct in our test. HTTPS uses the same property; there is nohttps.nonProxyHosts. - Windows settings are ignored by default.
-Djava.net.useSystemProxies=truemakes Java read the Windows, macOS or GNOME proxy settings. It is false by default and read once at startup. HttpClientreads the properties when it is built. Oracle says so directly: "The system-wide default values are retrieved at the time the HttpClient instance is constructed." Set them before you create the client.
java.net.http.HttpClient (Java 11 and later)
For one client instead of the whole JVM, pass a ProxySelector:
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("203.0.113.7", 8080)))
.connectTimeout(Duration.ofSeconds(15))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com/")).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
ProxySelector.of returns a selector "which uses the given proxy address for all HTTP and HTTPS requests". For an https:// page, the client asks the proxy for a tunnel with CONNECT, then speaks TLS with the site itself. The proxy carries the encrypted traffic without reading it. HttpClient.newHttpClient() with no selector uses the JVM properties from the section above, and in our test it reached the proxy that way too.
Proxy login, and the 407 that never clears
A proxy with a username and password answers the first request with 407 Proxy Authentication Required. Java retries with your credentials when an Authenticator supplies them for proxy challenges:
// First line of main: lets Java send a Basic login when it opens an HTTPS tunnel
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; // never hand proxy credentials to a website
}
})
.build();
The first line is the one that matters. Since Java 8u111, the JDK ships jdk.http.auth.tunneling.disabledSchemes=Basic, which turns off Basic login for HTTPS tunnels, because Basic sends the password in effectively cleartext. Our test shows what that looks like. On an https:// page, Java never asked the Authenticator, sent the CONNECT without a password and returned 407. It did so with HttpClient, with HttpURLConnection, and on Java 8. With the property cleared, the same requests returned 200 and the proxy saw the correct login.
When you clear it matters as well. System.setProperty as the first line of main worked for both clients. Clearing it after a first request did not: the retry got 407 again, even through a newly built HttpClient, and still sent no password. In our test the JDK kept the first value it saw, so clear it before anything touches the network, or pass it on the command line as -Djdk.http.auth.tunneling.disabledSchemes=. Our 407 guide covers the causes outside Java.
One more trap: http.proxyUser and http.proxyPassword are not JDK settings. With both set, both clients got a 407, and the proxy never saw a login. Gradle has settings with those names for its own downloads, covered below. For your application, the proxy login comes from an Authenticator.
HttpURLConnection and java.net.Proxy
Older code uses HttpURLConnection, which takes a java.net.Proxy for one connection:
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("203.0.113.7", 8080));
HttpURLConnection conn = (HttpURLConnection) URI.create("https://example.com/").toURL().openConnection(proxy);
conn.setConnectTimeout(15000);
conn.setReadTimeout(20000);
System.out.println(conn.getResponseCode());
A login works the same way here: an Authenticator installed with Authenticator.setDefault, and the tunnel setting cleared at startup. In our test, a failed tunnel login did not throw an exception either. getResponseCode() simply returned 407, so check the status before you read the body.
SOCKS5: where Java quietly goes wrong
SOCKS5 hides the most traps in Java, and two of them leak data you meant to keep behind the proxy.
HttpClient does not use SOCKS at all. Give it a SOCKS Proxy in its selector, or set -DsocksProxyHost, and it connects straight to the site from your own address. Our SOCKS server saw nothing while the request succeeded. The OpenJDK source explains it: the client keeps a proxy only if (p.type() == Proxy.Type.HTTP), and anything else becomes no proxy at all. There is no warning and no error.
HttpURLConnection does use SOCKS, with a Proxy object or with the socksProxyHost and socksProxyPort properties:
Proxy socks = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("198.51.100.14", 1080));
HttpURLConnection conn = (HttpURLConnection) URI.create("https://example.com/").toURL().openConnection(socks);
System.out.println(conn.getResponseCode());
Two details from the test. First, Java resolved the site name itself and sent the proxy an IP address, so the DNS lookup ran on your machine and your own resolver saw it. Our guide to SOCKS5 proxies explains why that split matters. Second, the login rules differ from those of HTTP proxies:
java.net.socks.usernameis ignored. It andjava.net.socks.passwordnever reached the proxy, on Java 17 or Java 8, although the Oracle properties page lists them. The OpenJDK code tries anAuthenticatorand then a fallback, nothing else.- The fallback is your account name. With no login supplied, Java sent the name of the Windows account the program ran under, with an empty password. Oracle documents this: "the user.name property will be used with no password."
- The usual
Authenticatorcheck fails. Java asks for a SOCKS5 login as requestor typeSERVERwith the protocolSOCKS5, so the common testgetRequestorType() == RequestorType.PROXYreturns nothing.
This Authenticator logged in on both Java versions:
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// Java asks for a SOCKS5 login with the protocol "SOCKS5" and the requestor type SERVER
if ("SOCKS5".equals(getRequestingProtocol())) {
return new PasswordAuthentication("user", "pass".toCharArray());
}
return null;
}
});
If you connect to our proxies from Java, generate HTTP lines, which is the default. Each line carries its username and password, and HTTP with an Authenticator is the path that worked in every Java client we tested.
OkHttp
OkHttp takes the same java.net.Proxy object, plus a separate authenticator for proxies:
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; // these credentials already failed, stop retrying
}
return response.request().newBuilder()
.header("Proxy-Authorization", Credentials.basic("user", "pass"))
.build();
})
.build();
Use proxyAuthenticator, not authenticator. OkHttp documents authenticator for "challenges from origin servers" and proxyAuthenticator for "challenges from proxy servers", and some tutorials put the proxy login in the wrong one. For SOCKS, pass Proxy.Type.SOCKS instead. OkHttp then opens the connection with the JDK's own socket, Socket(proxy) in its source, so the SOCKS5 login rules above apply to OkHttp as well. We read OkHttp from its source rather than running it, since it is a third-party library.
Rotate proxies with a ProxySelector
One address sending every request is the pattern rate limits catch. A ProxySelector that hands out the next proxy on each call rotates cleanly:
class RoundRobinSelector extends ProxySelector {
private final List<Proxy> pool;
private final AtomicInteger next = new AtomicInteger();
RoundRobinSelector(List<Proxy> pool) {
this.pool = pool;
}
@Override
public List<Proxy> select(URI uri) {
return List.of(pool.get(Math.floorMod(next.getAndIncrement(), pool.size())));
}
@Override
public void connectFailed(URI uri, SocketAddress address, IOException error) {
// called when a proxy cannot be reached; log it and retry the request
}
}
In our run, six requests through this selector reached three proxies twice each. Pass it with .proxy(new RoundRobinSelector(pool)) on HttpClient, or with .proxySelector(...) on OkHttp.
For a list to rotate, our free proxy API returns fresh addresses as plain text, no key needed:
String list = 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 = list.lines()
.map(String::trim)
.filter(line -> !line.isEmpty())
.map(line -> line.split(":"))
.map(hp -> new Proxy(Proxy.Type.HTTP, new InetSocketAddress(hp[0], Integer.parseInt(hp[1]))))
.collect(Collectors.toList());
Free proxies come and go: the free proxy list changes every few minutes as they die and new ones are found. For steady work, a residential gateway gives you one address and rotates the exit behind it.
Check the exit IP
A proxy setting that fails silently looks exactly like one that works, so compare the two addresses:
HttpClient direct = HttpClient.newHttpClient();
HttpClient proxied = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("203.0.113.7", 8080)))
.build();
HttpRequest whoAmI = HttpRequest.newBuilder(URI.create("https://api.ipify.org")).build();
String mine = direct.send(whoAmI, HttpResponse.BodyHandlers.ofString()).body();
String exit = proxied.send(whoAmI, HttpResponse.BodyHandlers.ofString()).body();
if (mine.equals(exit)) {
throw new IllegalStateException("the proxy is not in the path");
}
// Where the exit is and which network runs it (free, no key)
HttpRequest where = HttpRequest.newBuilder(URI.create("https://hproxy.com/api/ip/" + exit)).build();
System.out.println(direct.send(where, HttpResponse.BodyHandlers.ofString()).body());
The last request uses our free IP location API, which returns the country, city and network of any public address. The proxy checker runs the same test in a browser and adds anonymity and speed. Before you wire a new proxy into Java at all, the curl guide is the fastest check from a shell.
Maven and Gradle
Build tools do not see the flags of your application, so they need their own proxy setting.
Maven reads a <proxies> block in ~/.m2/settings.xml, and its nonProxyHosts uses the same format as the JDK:
<settings>
<proxies>
<proxy>
<id>office</id>
<active>true</active>
<protocol>http</protocol>
<host>203.0.113.7</host>
<port>8080</port>
<nonProxyHosts>localhost|*.internal.example</nonProxyHosts>
</proxy>
</proxies>
</settings>
Gradle takes systemProp lines in gradle.properties, with separate settings for HTTPS:
systemProp.http.proxyHost=203.0.113.7
systemProp.http.proxyPort=8080
systemProp.https.proxyHost=203.0.113.7
systemProp.https.proxyPort=8080
Gradle also reads systemProp.http.proxyUser and systemProp.http.proxyPassword for its own downloads. As our test showed, the same names do nothing for the requests your application makes.
Where to go from here
The same traps have close cousins in other stacks: the Python requests guide covers the equivalent settings there, and proxies for web scraping covers which proxy type a job needs. When the test phase ends, our pricing shows every product, and the API reference covers ordering and generating lines from code.
How we measured
Our lab script started local proxies on 127.0.0.1 that log everything reaching them. There was an HTTP proxy, one that demands a Basic login, a SOCKS5 server, one that demands a SOCKS5 login, and three more HTTP proxies for rotation. Each forwards to example.com, so a request through it completes, and a request that succeeds while no proxy logged anything went straight to the site. We ran 33 setups on the installed JDK 17.0.17 and Java 8u501 on Windows 11, each in a fresh JVM, twice on 27 September 2026, with identical results. For the SOCKS5 login we recorded whether the name matched the machine account, never the name itself. A second script compiled each plain Java example of this page with the JDK 17 compiler set to Java 11. OkHttp, Maven and Gradle were read from their source and documentation, not run.
Sources
- Oracle: Networking Properties (Java SE 17), HttpClient and HttpClient.Builder, ProxySelector, Java Networking and Proxies and the JDK 8u111 release notes.
- OpenJDK 17 source: net.properties, HttpRequestImpl.java and SocksSocketImpl.java.
- OkHttp 4.12.0 source: OkHttpClient.kt and RealConnection.kt.
- Apache Maven: Guide to using proxies. Gradle: Networking.


