Skip to content

Commit 38bb6f0

Browse files
committed
Fallback SOCKS proxies to HttpURLConnection
The JDK HttpClient does not support per-request SOCKS proxies, so dispatch those requests via HttpURLConnection instead of JDKs default of silently bypassing the proxy. Add a guard test and ensure the Java 11 HttpClient tests run. Fixes #2468
1 parent c1d0812 commit 38bb6f0

File tree

5 files changed

+135
-17
lines changed

5 files changed

+135
-17
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Parsing during charset sniffing no longer fails if an advisory `available()` call throws `IOException`, as seen on JDK 8 `HttpURLConnection`. [#2474](https://github.com/jhy/jsoup/issues/2474)
1212
* `Cleaner` no longer makes relative URL attributes in the input document absolute when cleaning or validating a `Document`. URL normalization now applies only to the cleaned output, and `Safelist.isSafeAttribute()` is side effect free. [#2475](https://github.com/jhy/jsoup/issues/2475)
1313
* `Cleaner` no longer duplicates enforced attributes when the input `Document` preserves attribute case. A case-variant source attribute is now replaced by the enforced attribute in the cleaned output. [#2476](https://github.com/jhy/jsoup/issues/2476)
14+
* If a per-request SOCKS proxy is configured, jsoup now avoids using the JDK `HttpClient`, because the JDK would silently ignore that proxy and attempt to connect directly. Those requests now fall back to the legacy `HttpURLConnection` transport instead, which does support SOCKS. [#2468](https://github.com/jhy/jsoup/issues/2468)
1415

1516
## 1.22.1 (2026-Jan-01)
1617

pom.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,13 +412,24 @@
412412
<phase>compile</phase>
413413
<goals>
414414
<goal>compile</goal>
415-
<goal>testCompile</goal>
416415
</goals>
417416
<configuration>
418417
<release>8</release>
419418
</configuration>
420419
</execution>
421420

421+
<execution>
422+
<id>testCompile-java-11</id>
423+
<phase>test-compile</phase>
424+
<goals>
425+
<goal>testCompile</goal>
426+
</goals>
427+
<configuration>
428+
<useModulePath>false</useModulePath>
429+
<release>11</release>
430+
</configuration>
431+
</execution>
432+
422433
<execution>
423434
<id>compile-java-11</id>
424435
<phase>compile</phase>

src/main/java/org/jsoup/helper/RequestDispatch.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static org.jsoup.helper.HttpConnection.Request;
77
import static org.jsoup.helper.HttpConnection.Response;
88

9+
import java.net.Proxy;
910
import java.lang.reflect.Constructor;
1011

1112
/**
@@ -35,6 +36,9 @@ static RequestExecutor get(Request request, @Nullable Response previousResponse)
3536

3637
if (request.sslSocketFactory() != null) // downgrade if a socket factory is set, as it can't be supplied to the HttpClient
3738
useHttpClient = false;
39+
Proxy proxy = request.proxy();
40+
if (proxy != null && proxy.type() == Proxy.Type.SOCKS) // HttpClient doesn't support SOCKS proxies
41+
useHttpClient = false;
3842

3943
if (useHttpClient && clientConstructor != null) {
4044
try {

src/test/java11/org/jsoup/helper/HttpClientExecutorTest.java

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,54 @@
11
package org.jsoup.helper;
2+
23
import org.jsoup.internal.SharedConstants;
34
import org.junit.jupiter.api.Test;
45

56
import java.io.IOException;
6-
import java.net.*;
7+
import java.net.InetSocketAddress;
8+
import java.net.Proxy;
9+
import java.net.ProxySelector;
10+
import java.net.SocketAddress;
11+
import java.net.URI;
712
import java.util.Collections;
813
import java.util.List;
914

1015
import static org.junit.jupiter.api.Assertions.*;
1116

1217
public class HttpClientExecutorTest {
18+
@Test void loadsMultiReleaseHttpClientExecutor() {
19+
// sanity check that the test is resolving the packaged Java 11 override, not a copy on the test classpath
20+
String resource = HttpClientTestAccess.executorClassResource().toExternalForm();
21+
assertTrue(resource.contains("/META-INF/versions/11/"), resource);
22+
}
23+
1324
@Test void getsHttpClient() {
1425
try {
1526
enableHttpClient();
1627
RequestExecutor executor = RequestDispatch.get(new HttpConnection.Request(), null);
17-
assertInstanceOf(HttpClientExecutor.class, executor);
28+
assertTrue(HttpClientTestAccess.isHttpClientExecutor(executor));
1829
} finally {
1930
disableHttpClient(); // reset to previous default for JDK8 compat tests
2031
}
2132
}
2233

23-
@Test void getsHttpUrlConnectionByDefault() {
34+
@Test void getsHttpClientByDefault() {
2435
System.clearProperty(SharedConstants.UseHttpClient);
2536
RequestExecutor executor = RequestDispatch.get(new HttpConnection.Request(), null);
26-
assertInstanceOf(HttpClientExecutor.class, executor);
37+
assertTrue(HttpClientTestAccess.isHttpClientExecutor(executor));
38+
}
39+
40+
@Test void downgradesSocksProxyToUrlConnectionExecutor() {
41+
try {
42+
enableHttpClient();
43+
HttpConnection.Request request = new HttpConnection.Request();
44+
request.proxy(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", 1080)));
45+
46+
// SOCKS handling only matters on the Java 11+ path where HttpClient would otherwise be selected (and just bypasses)
47+
RequestExecutor executor = RequestDispatch.get(request, null);
48+
assertInstanceOf(UrlConnectionExecutor.class, executor);
49+
} finally {
50+
disableHttpClient(); // reset to previous default for JDK8 compat tests
51+
}
2752
}
2853

2954
public static void enableHttpClient() {
@@ -51,9 +76,9 @@ public List<Proxy> select(URI uri) {
5176
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {}
5277
});
5378

54-
HttpClientExecutor.ProxyWrap wrap = new HttpClientExecutor.ProxyWrap();
79+
ProxySelector wrap = HttpClientTestAccess.newProxyWrap();
5580
List<Proxy> proxies = wrap.select(URI.create("http://example.com"));
56-
81+
5782
assertEquals(1, proxies.size());
5883
assertSame(defaultProxy, proxies.get(0).address());
5984
} finally {
@@ -62,12 +87,15 @@ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {}
6287
}
6388

6489
@Test void proxyWrapConnectFailedOnlyForSystemProxy() {
65-
HttpClientExecutor.ProxyWrap wrap = new HttpClientExecutor.ProxyWrap();
66-
HttpClientExecutor.perRequestProxy.set(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("custom", 9090)));
67-
wrap.connectFailed(URI.create("http://example.com"),
68-
new InetSocketAddress("custom", 9090),
69-
new IOException("test"));
70-
HttpClientExecutor.perRequestProxy.remove();
90+
try {
91+
ProxySelector wrap = HttpClientTestAccess.newProxyWrap();
92+
HttpClientTestAccess.setPerRequestProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("custom", 9090)));
93+
wrap.connectFailed(URI.create("http://example.com"),
94+
new InetSocketAddress("custom", 9090),
95+
new IOException("test"));
96+
} finally {
97+
HttpClientTestAccess.clearPerRequestProxy();
98+
}
7199
}
72100

73101
@Test
@@ -86,14 +114,14 @@ public List<Proxy> select(URI uri) {
86114
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {}
87115
});
88116

89-
HttpClientExecutor.perRequestProxy.set(
117+
HttpClientTestAccess.setPerRequestProxy(
90118
new Proxy(Proxy.Type.HTTP, perReqProxy));
91119

92-
HttpClientExecutor.ProxyWrap wrap = new HttpClientExecutor.ProxyWrap();
120+
ProxySelector wrap = HttpClientTestAccess.newProxyWrap();
93121
List<Proxy> proxies = wrap.select(URI.create("http://example.com"));
94122
assertSame(perReqProxy, proxies.get(0).address());
95123
} finally {
96-
HttpClientExecutor.perRequestProxy.remove();
124+
HttpClientTestAccess.clearPerRequestProxy();
97125
ProxySelector.setDefault(original);
98126
}
99127
}
@@ -108,7 +136,7 @@ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {}
108136
@Override
109137
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { called[0] = true; }
110138
});
111-
new HttpClientExecutor.ProxyWrap()
139+
HttpClientTestAccess.newProxyWrap()
112140
.connectFailed(URI.create("http://example.com"), new InetSocketAddress("x", 80), new IOException("x"));
113141
assertTrue(called[0]);
114142
} finally {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.jsoup.helper;
2+
3+
import java.lang.reflect.Constructor;
4+
import java.lang.reflect.Field;
5+
import java.net.Proxy;
6+
import java.net.ProxySelector;
7+
import java.net.URL;
8+
9+
/**
10+
Test access shim for the Java 11 multi-release classes.
11+
<p>These tests need to exercise the Java 11 implementation as loaded from
12+
{@code META-INF/versions/11}. Using reflection here keeps them bound to that
13+
packaged implementation, without adding the Java 11 sources to the test
14+
compile path.</p>
15+
*/
16+
final class HttpClientTestAccess {
17+
private static final String ExecutorClassName = "org.jsoup.helper.HttpClientExecutor";
18+
private static final String ProxyWrapClassName = ExecutorClassName + "$ProxyWrap";
19+
private static final String ExecutorClassResource = "org/jsoup/helper/HttpClientExecutor.class";
20+
21+
private HttpClientTestAccess() {}
22+
23+
static boolean isHttpClientExecutor(RequestExecutor executor) {
24+
return executorClass().isInstance(executor);
25+
}
26+
27+
static URL executorClassResource() {
28+
URL resource = HttpClientTestAccess.class.getClassLoader().getResource(ExecutorClassResource);
29+
if (resource == null)
30+
throw new IllegalStateException("Could not load " + ExecutorClassResource);
31+
return resource;
32+
}
33+
34+
static ProxySelector newProxyWrap() {
35+
try {
36+
Constructor<?> constructor = loadClass(ProxyWrapClassName).getDeclaredConstructor();
37+
constructor.setAccessible(true);
38+
return (ProxySelector) constructor.newInstance();
39+
} catch (ReflectiveOperationException e) {
40+
throw new IllegalStateException("Could not construct HttpClientExecutor.ProxyWrap", e);
41+
}
42+
}
43+
44+
static void setPerRequestProxy(Proxy proxy) {
45+
perRequestProxy().set(proxy);
46+
}
47+
48+
static void clearPerRequestProxy() {
49+
perRequestProxy().remove();
50+
}
51+
52+
@SuppressWarnings("unchecked")
53+
private static ThreadLocal<Proxy> perRequestProxy() {
54+
try {
55+
Field field = executorClass().getDeclaredField("perRequestProxy");
56+
field.setAccessible(true);
57+
return (ThreadLocal<Proxy>) field.get(null);
58+
} catch (ReflectiveOperationException e) {
59+
throw new IllegalStateException("Could not access HttpClientExecutor.perRequestProxy", e);
60+
}
61+
}
62+
63+
private static Class<?> executorClass() {
64+
return loadClass(ExecutorClassName);
65+
}
66+
67+
private static Class<?> loadClass(String className) {
68+
try {
69+
return Class.forName(className);
70+
} catch (ClassNotFoundException e) {
71+
throw new IllegalStateException("Could not load " + className, e);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)