-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Closed
Labels
bugBug in existing codeBug in existing code
Description
I have only noticed this issue after switching to okhttp v5:
The eventual timeout is the read timeout multiplied by the number of IPs (both IPv4 and IPv6) which the proxy address resolves to.
This happens when using a domain for proxyHost:
new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyHost, proxyPort)
i.e. being resilient against the proxy IPs changing over time.
This was previously discussed here:
#7698
Reproducer:
import okhttp3.Call;
import okhttp3.Dns;
import okhttp3.EventListener;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.assertj.core.api.Assertions.assertThat;
public class ReproduceOkHttpIssueTest {
@Test
public void test() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
StallingServer stallingServer = new StallingServer();
executorService.submit(() -> stallingServer.start(8080));
Thread.sleep(2000);
// 1. Define Hostname
String proxyHost = "unresponsive-proxy-host";
// 2. Set Timeouts
Duration callTimeout = Duration.ofSeconds(15);
Duration connectTimeout = Duration.ofSeconds(2);
Duration readTimeout = Duration.ofSeconds(10);
// 3. Build the Client with the Custom Dns
OkHttpClient client = new OkHttpClient.Builder()
.callTimeout(callTimeout)
.connectTimeout(connectTimeout)
.readTimeout(readTimeout)
.dns(hostname -> hostname.equals(proxyHost)
? List.of(InetAddress.getByName("127.0.0.1"), InetAddress.getByName("127.0.0.1"))
: Dns.SYSTEM.lookup(hostname))
.eventListener(new EventListener() {
@Override
public void connectStart(@NotNull Call call, @NotNull InetSocketAddress inetSocketAddress, @NotNull Proxy proxy) {
System.out.println("connect start - %s - inetSocketAddress: %s, proxy: %s".formatted(call.request().url(), inetSocketAddress, proxy));
}
})
.proxy(new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyHost, 8080)))
.build();
// 4. Test the Call
Request request = new Request.Builder().url("https://github.com/").build(); // Any valid target URL which is https
Instant startTime = Instant.now();
try (Response response = client.newCall(request).execute()) {
System.out.println("Call Succeeded unexpectedly.");
} catch (Exception e) {
Duration totalTime = Duration.between(startTime, Instant.now());
System.out.println("--- TEST RESULT ---");
System.out.println("Exception: " + e.getClass().getName() + ": " + e.getMessage());
System.out.println("Total Time: " + totalTime);
System.out.println("Expected Time (Call Timeout): " + callTimeout);
System.out.println("Observed Time (2 x Read Timeout): " + readTimeout.multipliedBy(2));
assertThat(totalTime).isLessThan(readTimeout.multipliedBy(2));
}
}
public static class StallingServer {
public void start(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Java server listening on port " + port);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Connection accepted from " + clientSocket.getInetAddress());
new Thread(() -> {
try (Socket sock = clientSocket) {
// 1. Read the client's initial request (e.g., "CONNECT google.com:443 HTTP/1.1")
InputStream in = sock.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
System.out.println("Received " + bytesRead + " bytes. Now stalling...");
// 2. Respond with "200 OK" to open the tunnel (Critical step for TLS simulation)
String successResponse = "HTTP/1.1 200 Connection established\r\n\r\n";
sock.getOutputStream().write(successResponse.getBytes());
sock.getOutputStream().flush();
System.out.println("Tunnel established on port " + port + ". Now stalling (TLS Handshake)...");
// 3. Block this thread indefinitely.
// The OkHttp client will now send the TLS ClientHello and hit the Read Timeout (10s) waiting for the ServerHello.
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
System.err.println("Connection handling error: " + e.getMessage());
}
}).start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}output:
Java server listening on port 8080
connect start - https://github.com/ - inetSocketAddress: /127.0.0.1:8080, proxy: HTTP @ unresponsive-proxy-host/<unresolved>:8080
Connection accepted from /127.0.0.1
Received 113 bytes. Now stalling...
Tunnel established on port 8080. Now stalling (TLS Handshake)...
connect start - https://github.com/ - inetSocketAddress: /127.0.0.1:8080, proxy: HTTP @ unresponsive-proxy-host/<unresolved>:8080
Connection accepted from /127.0.0.1
Received 113 bytes. Now stalling...
Tunnel established on port 8080. Now stalling (TLS Handshake)...
--- TEST RESULT ---
Exception: java.io.InterruptedIOException: timeout
Total Time: PT20.061722S
Expected Time (Call Timeout): PT15S
Observed Time (2 x Read Timeout): PT20S
Expecting actual:
20.061722S
to be less than:
20S
java.lang.AssertionError:
Expecting actual:
20.061722S
to be less than:
20S
at ReproduceOkHttpIssueTest.test(ReproduceOkHttpIssueTest.java:73)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
yschimke
Metadata
Metadata
Assignees
Labels
bugBug in existing codeBug in existing code