closeables;
@@ -791,6 +792,19 @@ public final HttpClientBuilder disableDefaultUserAgent() {
return this;
}
+ /**
+ * Disables automatic proxy detection for clients created by this builder.
+ *
+ * When disabled, and unless an explicit proxy or route planner is configured,
+ * the builder falls back to {@link org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner}.
+ *
+ * @return this instance.
+ */
+ public final HttpClientBuilder disableProxyAutodetection() {
+ this.proxyAutodetectionDisabled = true;
+ return this;
+ }
+
/**
* Sets the {@link ProxySelector} that will be used to select the proxies
* to be used for establishing HTTP connections. If a non-null proxy selector is set,
@@ -838,6 +852,7 @@ protected Function contextAdaptor() {
}
public CloseableHttpClient build() {
+
// Create main request executor
// We copy the instance fields to avoid changing them, and rename to avoid accidental use of the wrong version
HttpRequestExecutor requestExecCopy = this.requestExec;
@@ -1011,11 +1026,11 @@ public CloseableHttpClient build() {
}
if (proxy != null) {
routePlannerCopy = new DefaultProxyRoutePlanner(proxy, schemePortResolverCopy);
- } else if (this.proxySelector != null) {
- routePlannerCopy = new SystemDefaultRoutePlanner(schemePortResolverCopy, this.proxySelector);
- } else if (systemProperties) {
- final ProxySelector defaultProxySelector = AccessController.doPrivileged((PrivilegedAction) ProxySelector::getDefault);
- routePlannerCopy = new SystemDefaultRoutePlanner(schemePortResolverCopy, defaultProxySelector);
+ } else if (!this.proxyAutodetectionDisabled) {
+ final ProxySelector effectiveSelector = this.proxySelector != null
+ ? this.proxySelector
+ : AccessController.doPrivileged((PrivilegedAction) ProxySelector::getDefault);
+ routePlannerCopy = new SystemDefaultRoutePlanner(schemePortResolverCopy, effectiveSelector);
} else {
routePlannerCopy = new DefaultRoutePlanner(schemePortResolverCopy);
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/SystemDefaultRoutePlanner.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/SystemDefaultRoutePlanner.java
index 25566055c1..62bdb9b3be 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/SystemDefaultRoutePlanner.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/routing/SystemDefaultRoutePlanner.java
@@ -27,12 +27,16 @@
package org.apache.hc.client5.http.impl.routing;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
import java.util.List;
+import java.util.Locale;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.core5.annotation.Contract;
@@ -85,7 +89,7 @@ protected HttpHost determineProxy(final HttpHost target, final HttpContext conte
}
if (proxySelectorInstance == null) {
//The proxy selector can be "unset", so we must be able to deal with a null selector
- return null;
+ return determineEnvProxy(target); // === env-fallback ===
}
final List proxies = proxySelectorInstance.select(targetURI);
final Proxy p = chooseProxy(proxies);
@@ -100,8 +104,79 @@ protected HttpHost determineProxy(final HttpHost target, final HttpContext conte
result = new HttpHost(null, isa.getAddress(), isa.getHostString(), isa.getPort());
}
+ if (result == null) {
+ result = determineEnvProxy(target);
+ }
+
return result;
}
+ private static HttpHost determineEnvProxy(final HttpHost target) {
+ final boolean secure = "https".equalsIgnoreCase(target.getSchemeName());
+ HttpHost proxy = proxyFromEnv(secure ? "HTTPS_PROXY" : "HTTP_PROXY");
+ if (proxy == null && !secure) {
+ proxy = proxyFromEnv("HTTPS_PROXY"); // reuse HTTPS proxy for HTTP if only that exists
+ }
+ if (proxy != null && !isNoProxy(target)) {
+ return proxy;
+ }
+ return null;
+ }
+
+ private static HttpHost proxyFromEnv(final String var) {
+ String val = getenv(var);
+ if (val == null || val.isEmpty()) {
+ val = getenv(var.toLowerCase(Locale.ROOT));
+ }
+ if (val == null || val.isEmpty()) {
+ return null;
+ }
+ if (!val.contains("://")) {
+ val = "http://" + val;
+ }
+ try {
+ final URI uri = new URI(val);
+ final String host = uri.getHost();
+ final int port = uri.getPort() != -1
+ ? uri.getPort()
+ : ("https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80);
+ return new HttpHost(uri.getScheme(), InetAddress.getByName(host), port);
+ } catch (final Exception ignore) {
+ return null;
+ }
+ }
+
+ private static boolean isNoProxy(final HttpHost target) {
+ String list = getenv("NO_PROXY");
+ if (list == null || list.isEmpty()) {
+ list = getenv("no_proxy");
+ }
+ if (list == null || list.isEmpty()) {
+ return false;
+ }
+ final String host = target.getHostName().toLowerCase(Locale.ROOT);
+ final String hostPort = host + (target.getPort() != -1 ? ":" + target.getPort() : "");
+ for (String rule : list.split(",")) {
+ rule = rule.trim().toLowerCase(Locale.ROOT);
+ if (rule.isEmpty()) {
+ continue;
+ }
+ if (rule.equals(host) || rule.equals(hostPort)) {
+ return true; // exact
+ }
+ if (rule.startsWith("*.") && host.endsWith(rule.substring(1))) {
+ return true; // *.example.com
+ }
+ if (rule.endsWith("/16") && host.startsWith(rule.substring(0, rule.length() - 3))) {
+ return true; // cidr /16
+ }
+ }
+ return false;
+ }
+
+ private static String getenv(final String key) {
+ return AccessController.doPrivileged(
+ (PrivilegedAction) () -> System.getenv(key));
+ }
private Proxy chooseProxy(final List proxies) {
Proxy result = null;
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpClientBuilder.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpClientBuilder.java
index 2cec4699e2..29979f1dfa 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpClientBuilder.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestHttpClientBuilder.java
@@ -27,12 +27,22 @@
package org.apache.hc.client5.http.impl.classic;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.ProxySelector;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
+import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
+import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
+import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TestHttpClientBuilder {
@@ -66,4 +76,87 @@ public ClassicHttpResponse execute(
return chain.proceed(request, scope);
}
}
+
+ @Test
+ void testDefaultUsesSystemDefaultRoutePlanner() throws Exception {
+ try (final InternalHttpClient client = (InternalHttpClient) HttpClients.custom().build()) {
+ final Object planner = getPrivateField(client, "routePlanner");
+ Assertions.assertNotNull(planner);
+ Assertions.assertInstanceOf(SystemDefaultRoutePlanner.class, planner, "Default should be SystemDefaultRoutePlanner (auto-detect proxies)");
+ }
+ }
+
+ @Test
+ void testDisableProxyAutodetectionFallsBackToDefaultRoutePlanner() throws Exception {
+ try (final InternalHttpClient client = (InternalHttpClient) HttpClients.custom()
+ .disableProxyAutodetection()
+ .build()) {
+ final Object planner = getPrivateField(client, "routePlanner");
+ Assertions.assertNotNull(planner);
+ Assertions.assertInstanceOf(DefaultRoutePlanner.class, planner, "disableProxyAutodetection() should restore DefaultRoutePlanner");
+ }
+ }
+
+ @Test
+ void testExplicitProxyWinsOverAutodetection() throws Exception {
+ try (final InternalHttpClient client = (InternalHttpClient) HttpClients.custom()
+ .setProxy(new HttpHost("http", "proxy.local", 8080))
+ .build()) {
+ final Object planner = getPrivateField(client, "routePlanner");
+ Assertions.assertNotNull(planner);
+ Assertions.assertInstanceOf(DefaultProxyRoutePlanner.class, planner, "Explicit proxy must take precedence");
+ }
+ }
+
+ @Test
+ void testCustomRoutePlannerIsRespected() throws Exception {
+ final HttpRoutePlanner custom = new HttpRoutePlanner() {
+ @Override
+ public org.apache.hc.client5.http.HttpRoute determineRoute(
+ final HttpHost host, final HttpContext context) {
+ // trivial, never used in this test
+ return new org.apache.hc.client5.http.HttpRoute(host);
+ }
+ };
+ try (final InternalHttpClient client = (InternalHttpClient) HttpClients.custom()
+ .setRoutePlanner(custom)
+ .build()) {
+ final Object planner = getPrivateField(client, "routePlanner");
+ Assertions.assertSame(custom, planner, "Custom route planner must be used as-is");
+ }
+ }
+
+ @Test
+ void testProvidedProxySelectorIsUsedBySystemDefaultRoutePlanner() throws Exception {
+ class TouchProxySelector extends ProxySelector {
+ volatile boolean touched = false;
+ @Override
+ public java.util.List select(final java.net.URI uri) {
+ touched = true;
+ return java.util.Collections.singletonList(java.net.Proxy.NO_PROXY);
+ }
+ @Override
+ public void connectFailed(final java.net.URI uri, final java.net.SocketAddress sa, final IOException ioe) { }
+ }
+ final TouchProxySelector selector = new TouchProxySelector();
+
+ try (final InternalHttpClient client = (InternalHttpClient) HttpClients.custom()
+ .setProxySelector(selector)
+ .build()) {
+ final Object planner = getPrivateField(client, "routePlanner");
+ Assertions.assertInstanceOf(SystemDefaultRoutePlanner.class, planner);
+
+ // Call determineRoute on the planner directly to avoid making a real request
+ final SystemDefaultRoutePlanner sdrp = (SystemDefaultRoutePlanner) planner;
+ sdrp.determineRoute(new HttpHost("http", "example.com", 80), HttpClientContext.create());
+
+ Assertions.assertTrue(selector.touched, "Provided ProxySelector should be consulted");
+ }
+ }
+
+ private static Object getPrivateField(final Object target, final String name) throws Exception {
+ final Field f = target.getClass().getDeclaredField(name);
+ f.setAccessible(true);
+ return f.get(target);
+ }
}
\ No newline at end of file
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestSystemDefaultRoutePlanner.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestSystemDefaultRoutePlanner.java
index 29260551f5..82cd504254 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestSystemDefaultRoutePlanner.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/routing/TestSystemDefaultRoutePlanner.java
@@ -27,12 +27,15 @@
package org.apache.hc.client5.http.impl.routing;
+import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
+
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import org.apache.hc.client5.http.HttpRoute;
@@ -42,9 +45,12 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
+
/**
* Tests for {@link SystemDefaultRoutePlanner}.
*/
@@ -91,8 +97,8 @@ void testDirectDefaultPort() throws Exception {
@Test
void testProxy() throws Exception {
- final InetAddress ia = InetAddress.getByAddress(new byte[] {
- (byte)127, (byte)0, (byte)0, (byte)1
+ final InetAddress ia = InetAddress.getByAddress(new byte[]{
+ (byte) 127, (byte) 0, (byte) 0, (byte) 1
});
final InetSocketAddress isa1 = new InetSocketAddress(ia, 11111);
final InetSocketAddress isa2 = new InetSocketAddress(ia, 22222);
@@ -113,4 +119,52 @@ void testProxy() throws Exception {
Assertions.assertEquals(isa1.getPort(), route.getProxyHost().getPort());
}
+ @EnabledForJreRange(max = JRE.JAVA_15)
+ @Test
+ void testEnvHttpProxy() throws Exception {
+ withEnvironmentVariable("HTTP_PROXY", "http://proxy.acme.local:8080")
+ .execute(() -> {
+ Mockito.when(proxySelector.select(ArgumentMatchers.any()))
+ .thenReturn(Collections.singletonList(Proxy.NO_PROXY));
+
+ final HttpHost target = new HttpHost("http", "example.com", 80);
+ final HttpRoute route = routePlanner.determineRoute(
+ target, HttpClientContext.create());
+
+ Assertions.assertNull(route.getProxyHost());
+ });
+ }
+ @EnabledForJreRange(max = JRE.JAVA_15)
+ @Test
+ void testEnvHttpsProxy() throws Exception {
+ withEnvironmentVariable("HTTPS_PROXY", "http://secure.proxy:8443")
+ .execute(() -> {
+ Mockito.when(proxySelector.select(ArgumentMatchers.any()))
+ .thenReturn(Collections.singletonList(Proxy.NO_PROXY));
+
+ final HttpHost target = new HttpHost("https", "secure.example", 443);
+ final HttpRoute route = routePlanner.determineRoute(
+ target, HttpClientContext.create());
+
+ Assertions.assertNull(route.getProxyHost());
+ });
+ }
+ @EnabledForJreRange(max = JRE.JAVA_15)
+ @Test
+ void testEnvNoProxyExcludesHost() throws Exception {
+ withEnvironmentVariable("HTTP_PROXY", "http://proxy:3128")
+ .and("NO_PROXY", "localhost,127.0.0.1")
+ .execute(() -> {
+ Mockito.when(proxySelector.select(ArgumentMatchers.any()))
+ .thenReturn(Collections.singletonList(Proxy.NO_PROXY));
+
+ final HttpHost target = new HttpHost("http", "localhost", 80);
+ final HttpRoute route = routePlanner.determineRoute(
+ target, HttpClientContext.create());
+
+ Assertions.assertNull(route.getProxyHost());
+ });
+ }
+
+
}
diff --git a/pom.xml b/pom.xml
index f748421420..cd3453f76e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,7 @@
javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer
1.27.1
1.5.7-4
+ 1.2.1
@@ -216,6 +217,11 @@
zstd-jni
${zstd.jni.version}
+
+ com.github.stefanbirkner
+ system-lambda
+ ${stefanbirkner.version}
+