SocksProxyTest.java
/*
* Copyright (c) 2024-2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.asynchttpclient.proxy;
import io.github.artsok.RepeatedIfExceptionsTest;
import org.asynchttpclient.AbstractBasicTest;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.Response;
import org.asynchttpclient.testserver.SocksProxy;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.ServerSocket;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for SOCKS proxy support with both HTTP and HTTPS.
* Validates fix for GitHub issue #2139 (SOCKS proxy support broken).
*/
public class SocksProxyTest extends AbstractBasicTest {
@Override
public AbstractHandler configureHandler() throws Exception {
return new ProxyTest.ProxyHandler();
}
/**
* Returns a port that is not in use by binding to port 0 and then closing the socket.
*/
private static int findFreePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
@RepeatedIfExceptionsTest(repeats = 5)
public void testSocks4ProxyWithHttp() throws Exception {
// Start SOCKS proxy in background thread
Thread socksProxyThread = new Thread(() -> {
try {
new SocksProxy(60000);
} catch (Exception e) {
logger.error("Failed to establish SocksProxy", e);
}
});
socksProxyThread.start();
// Give the proxy time to start
Thread.sleep(1000);
try (AsyncHttpClient client = asyncHttpClient()) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V4))
.execute();
Response response = f.get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertEquals(200, response.getStatusCode());
}
}
/**
* Validates that when a SOCKS5 proxy is configured at an address where no
* SOCKS server is running, the HTTP request FAILS instead of silently
* bypassing the proxy and using normal routing.
* This is the core regression test for GitHub issue #2139.
*/
@Test
public void testSocks5ProxyNotRunningMustFailHttp() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V5))
.execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Request should fail when SOCKS5 proxy is not running, not bypass proxy");
}
}
/**
* Validates that when a SOCKS4 proxy is configured at an address where no
* SOCKS server is running, the HTTP request FAILS instead of silently
* bypassing the proxy and using normal routing.
*/
@Test
public void testSocks4ProxyNotRunningMustFailHttp() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V4))
.execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Request should fail when SOCKS4 proxy is not running, not bypass proxy");
}
}
/**
* Validates that when a SOCKS5 proxy is configured at an address where no
* SOCKS server is running, an HTTPS request FAILS instead of silently
* bypassing the proxy and using normal routing.
*/
@Test
public void testSocks5ProxyNotRunningMustFailHttps() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "https://localhost:" + port2 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V5))
.execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Request should fail when SOCKS5 proxy is not running, not bypass proxy");
}
}
/**
* Validates that when a SOCKS4 proxy is configured at an address where no
* SOCKS server is running, an HTTPS request FAILS instead of silently
* bypassing the proxy and using normal routing.
*/
@Test
public void testSocks4ProxyNotRunningMustFailHttps() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "https://localhost:" + port2 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V4))
.execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Request should fail when SOCKS4 proxy is not running, not bypass proxy");
}
}
/**
* Validates that per-request SOCKS5 proxy config with a non-existent proxy
* also correctly fails the request.
*/
@Test
public void testPerRequestSocks5ProxyNotRunningMustFail() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V5))
.execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Per-request SOCKS5 proxy config should not be silently ignored");
}
}
/**
* Validates that client-level SOCKS5 proxy config with a non-existent proxy
* also correctly fails the request.
*/
@Test
public void testClientLevelSocks5ProxyNotRunningMustFail() throws Exception {
int freePort = findFreePort();
try (AsyncHttpClient client = asyncHttpClient(config()
.setProxyServer(new ProxyServer.Builder("127.0.0.1", freePort)
.setProxyType(ProxyType.SOCKS_V5))
.setConnectTimeout(Duration.ofMillis(5000))
.setRequestTimeout(Duration.ofMillis(10000)))) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target).execute();
assertThrows(ExecutionException.class, () -> f.get(10, TimeUnit.SECONDS),
"Client-level SOCKS5 proxy config should not be silently ignored");
}
}
}