Guide

How to Use Proxies With Java (HttpClient, java.net.Proxy and OkHttp)

Java proxy settings tested on Java 17 and 8: the https.proxyHost trap, the 407 that never clears over HTTPS, and why HttpClient silently skips SOCKS5.

HProxy Team··Updated September 27, 2026·10 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

Proxies for Web Scraping→

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.proxyHost with http.proxyPort, and https.proxyHost with https.proxyPort.
  • For one HttpClient, pass ProxySelector.of(new InetSocketAddress(host, port)).
  • For a proxy login over HTTPS, clear jdk.http.auth.tunneling.disabledSchemes as the first line of main, then add an Authenticator.
  • For SOCKS5, use HttpURLConnection or OkHttp, never HttpClient, and answer the login for the protocol SOCKS5.
SetupWhat happened in our test
-Dhttp.proxyHost only, an https:// pageWent straight to the site
-Dhttps.proxyHost, an https:// pageWent through the proxy
HTTPS_PROXY environment variableIgnored, went straight to the site
Login proxy, https://, default settings407, Java never sent the password
The same, disabledSchemes cleared200 through the proxy
HttpClient given a SOCKS proxyIgnored, went straight to the site
java.net.socks.username setIgnored, the SOCKS5 login failed
An Authenticator answering SOCKS5Logged in, 200 through the proxy
Our own console window: the Java proxy lab printing its result, one line per setup, with the status and which proxy saw the request, on Java 17 and Java 8.
Captured on our own machine on 27 September 2026: our measurement script printing its saved result. All proxies ran on 127.0.0.1.

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_PROXY and HTTP_PROXY set, both Java clients went straight to the site. The JDK does not read them.
  • Local addresses skip the proxy. http.nonProxyHosts defaults to localhost|127.*|[::1], and a page on localhost went direct in our test. HTTPS uses the same property; there is no https.nonProxyHosts.
  • Windows settings are ignored by default. -Djava.net.useSystemProxies=true makes Java read the Windows, macOS or GNOME proxy settings. It is false by default and read once at startup.
  • HttpClient reads 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.username is ignored. It and java.net.socks.password never reached the proxy, on Java 17 or Java 8, although the Oracle properties page lists them. The OpenJDK code tries an Authenticator and 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 Authenticator check fails. Java asks for a SOCKS5 login as requestor type SERVER with the protocol SOCKS5, so the common test getRequestorType() == RequestorType.PROXY returns 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

Frequently asked questions

Why does proxy authentication fail over HTTPS in Java?
Because the JDK switches off Basic login for HTTPS tunnels by default: it ships jdk.http.auth.tunneling.disabledSchemes=Basic. In our test on Java 17 and Java 8, Java never asked the Authenticator, sent the CONNECT without a password and returned 407. Clear the property as the first line of main, System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""), or with -Djdk.http.auth.tunneling.disabledSchemes= on the command line. Clearing it after a first request did not help.
Does Java use the HTTP_PROXY and HTTPS_PROXY environment variables?
No. With both variables set, HttpClient and HttpURLConnection went straight to the site in our test. Java takes a proxy from the http.proxyHost and https.proxyHost system properties, from a ProxySelector, or from a java.net.Proxy object, and from the operating system settings only if java.net.useSystemProxies is true.
Does java.net.http.HttpClient support SOCKS proxies?
No, and it does not warn you. With a SOCKS proxy in its selector, or with -DsocksProxyHost set, HttpClient connected straight to the site from our own address in our test. The OpenJDK source keeps a proxy only if it is of type HTTP. Use HttpURLConnection or OkHttp for SOCKS5, or an HTTP proxy with HttpClient.
Why does my SOCKS5 username not work in Java?
Java does not read java.net.socks.username and java.net.socks.password, although Oracle lists them: in our test they never reached the proxy on Java 17 or Java 8. Java asks the Authenticator for a SOCKS5 login with the protocol SOCKS5 and the requestor type SERVER, so answer when getRequestingProtocol() is SOCKS5. Without a login, Java sends your system account name with an empty password.
What is the default value of http.nonProxyHosts?
localhost|127.*|[::1]. Hosts on that list skip the proxy, and HTTPS uses the same property, since there is no https.nonProxyHosts. The value is a list separated by the | character, with * as a wildcard, for example -Dhttp.nonProxyHosts="*.internal.example|localhost".
How do I make Java use the Windows proxy settings?
Start the JVM with -Djava.net.useSystemProxies=true. Java then reads the proxy settings of Windows, macOS or GNOME. It is false by default and read only once at startup, and the http.proxyHost style properties still take precedence when they are set.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed