Search code examples
javaapache-httpclient-4.x

Apache HttpClients always takes 2x the configured ConnectTimeout to actually timeout?


Example code:

public static void main(String[] args) throws Exception {
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(1_000)
            .setSocketTimeout(5_000)
            .setConnectionRequestTimeout(60_000)
            .build();
    
    CloseableHttpClient httpClient = HttpClients.custom()
            .disableAutomaticRetries()
            .disableRedirectHandling()
            .setDefaultRequestConfig(requestConfig)
            .build();
    
    HttpHead httpHead = new HttpHead("http://dns.google/a.mp4");
    httpHead.setHeader("Accept-Encoding", "identity");
    
    final long startTime = System.currentTimeMillis();
    try (CloseableHttpResponse httpResponse = httpClient.execute(httpHead)) {
        final long elapsedTime = System.currentTimeMillis() - startTime;
        System.out.println("Success after " + elapsedTime + " ms");
    } catch (Exception e) {
        final long elapsedTime = System.currentTimeMillis() - startTime;
        System.err.println("Error after " + elapsedTime + " ms");
        e.printStackTrace();
    }
}

This HttpHead is pointing to a URL that doesn't exist (http://dns.google/a.mp4 in this case), so this request should timeout after 1 second, right? But instead it's taking 2 seconds to timeout:

Error after 2064 ms
java.net.NoRouteToHostException: No route to host
    at java.base/sun.nio.ch.Net.connect0(Native Method)
    at java.base/sun.nio.ch.Net.connect(Net.java:579)
    at java.base/sun.nio.ch.Net.connect(Net.java:568)
    at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:593)
    at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
    at java.base/java.net.Socket.connect(Socket.java:633)
    at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75)
    at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142)
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)

No matter what I change the ConnectTimeout to, it always takes 2x that amount of time to timeout. Why???

I'm using HttpClients version 4.5.13


Solution

  • So in the DefaultHttpClientConnectionOperator class you’ll see this code:

    final InetAddress[] addresses = host.getAddress() != null ?
            new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName());
    ...
    for (int i = 0; i < addresses.length; i++) {
        ...
        try {
            sock = sf.connectSocket(connectTimeout, sock, host, remoteAddress, localAddress, context);
            ...
        } catch (final SocketTimeoutException ex) {
            if (last) {
                throw new ConnectTimeoutException(ex, host, addresses);
            }
        } catch (final NoRouteToHostException ex) {
            if (last) {
                throw ex;
            }
        }
        ...
    }
    

    When you debug it, you'll see that this is the value of addresses:

    [dns.google/8.8.4.4, dns.google/8.8.8.8, dns.google/2001:4860:4860:0:0:0:0:8888, dns.google/2001:4860:4860:0:0:0:0:8844]
    

    So it’s actually making 4 requests... the first 2 fail with a SocketTimeoutException after 1 second each, and the second 2 fail with a NoRouteToHostException pretty much immediately. I don’t know why the last 2 requests fail immediately, but they do.

    So the end result is that it takes approximately 2 seconds to make all the requests, and the end result is a NoRouteToHostException