Correct Way To Set Timeout In RestTemplate

TLDR;

If you set connection timeout, you also need to explicitly set read timeout (to 0 for infinite timeout)

Overview

Setting timeout for HTTP connection is something most developers don’t do. However, it is vital you set this configuration right when you have to.

Connection Timeout and Socket Timeout

In layman’s terms, connection timeout governs the time taken to establish a connection between hosts. If you remember the TCP handshake, a connection timeout was applied to this phase.

Socket Timeout (Read Timeout) governs the later phase when the connection is already established. It signifies how long the client is willing to wait for the server to return the response. If the client sets the Socket timeout to 10 seconds and the server takes more than that to respond, the client will throw a SocketTimeoutException.

Many times, you don’t want to set Socket Timeout for various reasons. However, you may want to set a connection timeout. Normally, if a connection cannot be made after a few seconds, probably the other host is down and there is little value in waiting longer.

Reasonably, you would only set Connection Timeout and leave the Socket timeout alone hoping things will work as expected: if a connection cannot be established after a few seconds, drop the connection, otherwise, wait indefinitely for the result.

This is where things go wrong, especially with this popular HTTP client library (used by Spring) https://github.com/apache/httpcomponents-client/tree/rel/v4.5.13

The danger of setting only connection timeout

Consider the following seemingly harmless code:

    @Override
    public void run(String... args) throws Exception {
        var restTemplate = new RestTemplate();

        var requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setConnectTimeout(50);
        //configure connection timeout using http factory
        restTemplate.setRequestFactory(requestFactory);

        restTemplate.getForObject("https://edition.cnn.com/", String.class);
    }

Here, I only set the connection timeout to 50 milliseconds (a bit too little, I know). However, the error looks very interesting:

Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://edition.cnn.com/": Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:791) ~[spring-web-5.3.31.jar:5.3.31]
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:717) ~[spring-web-5.3.31.jar:5.3.31]
        at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:340) ~[spring-web-5.3.31.jar:5.3.31]
        at com.datmt.learning.java.springboottest.SpringBootTestApplication.run(SpringBootTestApplication.java:25) ~[classes/:na]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:765) ~[spring-boot-2.7.18.jar:2.7.18]
        ... 17 common frames omitted
Caused by: java.net.SocketTimeoutException: Read timed out
        at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:278) ~[na:na]
        at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:304) ~[na:na]
        at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346) ~[na:na]
        at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796) ~[na:na]
        at java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099) ~[na:na]
        at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489) ~[na:na]
        at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483) ~[na:na]
        at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70) ~[na:na]
        at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1461) ~[na:na]
        at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1066) ~[na:na]
        at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137) ~[httpcore-4.4.16.jar:4.4.16]
        at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153) ~[httpcore-4.4.16.jar:4.4.16]
        at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280) ~[httpcore-4.4.16.jar:4.4.16]
        at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138) ~[httpclient-4.5.13.jar:4.5.13]
        at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56) ~[httpclient-4.5.13.jar:4.5.13]
        at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259) ~[httpcore-4.4.16.jar:4.4.16]
        at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163) ~[httpcore-4.4.16.jar:4.4.16]

It’s a Socket/Read timeout, not a connection timeout. That means the connection was already established.

But I didn’t set a read timeout, should that be infinite?

That’s not the case.

The culprits

Let’s take a look at the following code blocks following the stack trace:

            if (timeout > 0) {
                // read with timeout
                n = timedRead(fd, b, off, len, MILLISECONDS.toNanos(timeout));
            } else {
                // read, no timeout
                n = tryRead(fd, b, off, len);
                while (IOStatus.okayToRetry(n) && isOpen()) {
                    park(fd, Net.POLLIN);
                    n = tryRead(fd, b, off, len);
                }
            }

You can see that, the timeout was set and the value was 50 (milliseconds)!

Let’s follow the code:
SSLConnectionSocketFactory:

    public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException {
        Args.notNull(host, "HTTP host");
        Args.notNull(remoteAddress, "Remote address");
        Socket sock = socket != null ? socket : this.createSocket(context);
        if (localAddress != null) {
            sock.bind(localAddress);
        }

        try {
            if (connectTimeout > 0 && sock.getSoTimeout() == 0) {
                sock.setSoTimeout(connectTimeout);
            }

            if (this.log.isDebugEnabled()) {
                this.log.debug("Connecting socket to " + remoteAddress + " with timeout " + connectTimeout);
            }

            sock.connect(remoteAddress, connectTimeout);
        } catch (IOException ex) {
            try {
                sock.close();
            } catch (IOException var10) {
            }

            throw ex;
        }

        if (sock instanceof SSLSocket) {
            SSLSocket sslsock = (SSLSocket)sock;
            this.log.debug("Starting handshake");
            sslsock.startHandshake();
            this.verifyHostname(sslsock, host.getHostName());
            return sock;
        } else {
            return this.createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
        }
    }

Pay very close attention to line 10, this is where connect timeout was used to set to socket timeout. By default, the value of socket timeout and connect timeout in HttpComponentsClientHttpRequestFactory is -1.

This -1 value is very important as we inspect the next code from MainClientExec.class

                int timeout = config.getSocketTimeout();
                if (timeout >= 0) {
                    managedConn.setSocketTimeout(timeout);
                }

Here, only if the config.getSocketTimeout() >= 0, it will be set. This is the final point where the socket timeout is set before the request is executed in NioSocketImpl. However, since the default config for this timeout is -1, the socket timeout in managedConn was not modified. It remains the value that was set before = connection timeout.

How do you set socket timeout and connect timeout correctly?

As you can see, setting connect timeout without setting socket timeout makes your app behavior incorrectly and unexpectedly.

To prevent this, you need to also set the read timeout to 0 or to a positive value that fits your application needs.

    @Override
    public void run(String... args) throws Exception {
        var restTemplate = new RestTemplate();

        var requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setConnectTimeout(50);
        requestFactory.setReadTimeout(0);
        //configure connection timeout using http factory
        restTemplate.setRequestFactory(requestFactory);

        restTemplate.getForObject("https://edition.cnn.com/", String.class);
    }

Now the application runs without any issues

Conclusion

In this post, I’ve shown you how to correctly set the connect timeout and read timeout when using restTempalte. This also applies to other libraries that use httpclient. Why the httpclient developer does this is beyond my knowledge. You can read about this open issue here: https://issues.apache.org/jira/browse/HTTPCLIENT-2091

Leave a Comment