Guide

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

How to route Java requests through a proxy: the JVM system properties, java.net.http.HttpClient with ProxySelector, java.net.Proxy for HttpURLConnection, OkHttp's proxy and proxyAuthenticator, SOCKS5, the Basic-auth-over-tunnel gotcha, and verifying your exit IP.

HProxy Team · ·Updated July 17, 2026 ·8 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.65/GB, pay as you go.

See plans & pricing

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 (default localhost|127.*|[::1]), socksProxyHost/socksProxyPort (default 1080), socksProxyVersion (default 5), and java.net.socks.username/password.
  • java.net.Proxy and ProxySelector (Java SE 21): the Proxy(Proxy.Type, SocketAddress) constructor, the DIRECT/HTTP/SOCKS types, and ProxySelector.of(InetSocketAddress) returning a selector for HTTP/HTTPS.
  • OpenJDK net.properties: jdk.http.auth.tunneling.disabledSchemes=Basic disabling Basic auth for HTTPS CONNECT tunneling through a proxy.
  • OkHttpClient.Builder source (OkHttp 4.12.0): proxy(Proxy?) taking precedence over proxySelector(ProxySelector), and proxyAuthenticator(Authenticator) responding to proxy challenges as opposed to authenticator for origin servers.

Frequently asked questions

Why does proxy Basic authentication fail over HTTPS in Java?
Because the JDK disables Basic auth for HTTPS tunnels by default. The property jdk.http.auth.tunneling.disabledSchemes ships set to Basic, which means when Java opens a CONNECT tunnel to reach an https:// site through a proxy, it refuses to answer the proxy's Basic challenge, and you get a 407 that never clears. Remove Basic from that list to fix it, for example -Djdk.http.auth.tunneling.disabledSchemes="" on the command line or System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "") before you build the client. This is the single most common Java proxy-auth surprise.
Does java.net.http.HttpClient support SOCKS proxies?
No. The modern HttpClient takes a ProxySelector, and ProxySelector.of builds a selector for HTTP and HTTPS proxies only. There is no SOCKS path through it. For SOCKS you have three options: set the socksProxyHost and socksProxyPort system properties, which affect the socket layer used by HttpURLConnection and plain sockets, construct a java.net.Proxy of type SOCKS for a URLConnection, or use OkHttp with a SOCKS Proxy object.
What is the difference between setting system properties and setting a proxy per client?
System properties like http.proxyHost and https.proxyHost are JVM-global: every HTTP connection in the process that respects them goes through that proxy. A ProxySelector on a java.net.http.HttpClient, a java.net.Proxy on a URLConnection, or a proxy on an OkHttpClient is scoped to that one client. Use properties for a whole-process default and the per-client APIs when different parts of your app need different exits.
How do I authenticate to a proxy in Java?
For java.net.http.HttpClient and HttpURLConnection, install a java.net.Authenticator whose getPasswordAuthentication returns the credentials only when getRequestorType() is RequestorType.PROXY. For OkHttp, set a proxyAuthenticator that adds a Proxy-Authorization header built with Credentials.basic(user, pass). For SOCKS5 via system properties, set java.net.socks.username and java.net.socks.password. A rejected login returns HTTP 407, which means the proxy is alive and only the credentials are wrong.
What is the default value of http.nonProxyHosts?
localhost|127.*|[::1]. It is a list of hosts that bypass the proxy, separated by the pipe character, and it accepts a * wildcard, so -Dhttp.nonProxyHosts="*.internal.example|localhost" sends internal traffic direct. HTTPS shares this same property rather than having its own, so setting it once covers both schemes.

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.

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