Skip to content

Call Timeout not obeyed when proxy resolves to multiple IPs and TLS handshake stalls #9191

@elrob

Description

@elrob

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug in existing code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions