Spring RestTemplate Connection Timeout is not working

Easy2DownVoteHard2Ans picture Easy2DownVoteHard2Ans · May 11, 2017 · Viewed 31.5k times · Source

I am trying to configure time out when external web service call. I am calling external web service by Spring Rest Template in my service.

For connection timeout testing purpose, the external web service is stopped and application server is down.

I have configured 10 seconds for timeout, but unfortunately i get connection refused exception after a second.

try {   
    final RestTemplate restTemplate = new RestTemplate();

    ((org.springframework.http.client.SimpleClientHttpRequestFactory)
        restTemplate.getRequestFactory()).setReadTimeout(1000*10);

    ((org.springframework.http.client.SimpleClientHttpRequestFactory)
        restTemplate.getRequestFactory()).setConnectTimeout(1000*10);

    HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

    HttpEntity<String> entity = new HttpEntity<String>(reqJSON, headers);

    ResponseEntity<String> response = restTemplate.exchange(wsURI, HttpMethod.POST, entity, String.class);

    String premiumRespJSONStr = response.getBody();
}

Please correct my understanding if any.

Answer

andreim picture andreim · May 11, 2017

The following is related to connectTimeout setting.

Case - unknown host

If you have a host that is not reachable (eg: http://blablablabla/v1/timeout) then you will receive UnknownHostException as soon as possible. AbstractPlainSocketImpl :: connect() :: !addr.isUnresolved() :: throw UnknownHostException without any timeout. The host is resolved using InetAddress.getByName(<host_name>).

Case - unknown port

If you have a host that is reachable but no connection can be done then you receive ConnectException - Connection refused: connect as soon as possible. It seems that this happens in a native method DualStackPlainSocketImpl :: static native void waitForConnect(int fd, int timeout) throws IOException which is called from DualStackPlainSocketImpl :: socketConnect(). The timeout is not respected.

Proxy? if a proxy is used things might change. Having a reachable proxy you might get the timeout.

Related questions check this answer as is related to the case you are encountering.

DNS Round-Robin having the same domain mapped to multiple IP addresses will cause the client to connect to each of the IPs until one is found. Therefore the connectTimeout() will add its own penalty for each of the IPs in the list which are not working. Read this article for more.

Conclusion if you want to obtain the connectTimeout then you might need to implement your own retry logic or use a proxy.

Testing connectTimeout you can refer to this answer of various ways of having an endpoint that prevents a socket connection from completing thus obtaining a timeout. Choosing a solution, you can create an integration test in spring-boot which validates your implementation. This can be similar to the next test used for testing the readTimeout, just that for this case the URL can be changed into one that prevents a socket connection.

Testing readTimeout

In order to test the readTimeout there need to be a connection first, therefore the service needs to be up. Then an endpoint can be provided that, for each request, returns a response with a large delay.

The following can be done in spring-boot in order to create an integration test:

1. Create the test

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = { RestTemplateTimeoutConfig.class, RestTemplateTimeoutApplication.class }
)
public class RestTemplateTimeoutTests {

    @Autowired
    private RestOperations restTemplate;

    @LocalServerPort
    private int port;

    @Test
    public void resttemplate_when_path_exists_and_the_request_takes_too_long_throws_exception() {
        System.out.format("%s - %s\n", Thread.currentThread().getName(), Thread.currentThread().getId());

        Throwable throwable = catchThrowable(() ->
                restTemplate.getForEntity(String.format("http://localhost:%d/v1/timeout", port), String.class));

        assertThat(throwable).isInstanceOf(ResourceAccessException.class);
        assertThat(throwable).hasCauseInstanceOf(SocketTimeoutException.class);
    }
}

2. Configure RestTemplate

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(getRequestFactory());
    }

    private ClientHttpRequestFactory getRequestFactory() {
        HttpComponentsClientHttpRequestFactory factory =
                new HttpComponentsClientHttpRequestFactory();

        factory.setReadTimeout(TIMEOUT);
        factory.setConnectTimeout(TIMEOUT);
        factory.setConnectionRequestTimeout(TIMEOUT);
        return factory;
    }
}

3. Spring Boot app that will be run when the test starts

@SpringBootApplication
@Controller
@RequestMapping("/v1/timeout")
public class RestTemplateTimeoutApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestTemplateTimeoutApplication.class, args);
    }

    @GetMapping()
    public @ResponseStatus(HttpStatus.NO_CONTENT) void getDelayedResponse() throws InterruptedException {
        System.out.format("Controller thread = %s - %s\n", Thread.currentThread().getName(), Thread.currentThread().getId());
        Thread.sleep(20000);
    }
}

Alternative ways of configuring the RestTemplate

Configure existing RestTemplate

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    // consider that this is the existing RestTemplate
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    // this will change the RestTemplate settings and create another bean
    @Bean
    @Primary
    public RestTemplate newRestTemplate(RestTemplate restTemplate) {
        SimpleClientHttpRequestFactory factory =
                (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();

        factory.setReadTimeout(TIMEOUT);
        factory.setConnectTimeout(TIMEOUT);

        return restTemplate;
    }
}

Configure a new RestTemplate using RequestConfig

@Configuration
public class RestTemplateTimeoutConfig {

    private final int TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10);

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(getRequestFactoryAdvanced());
    }

    private ClientHttpRequestFactory getRequestFactoryAdvanced() {
        RequestConfig config = RequestConfig.custom()
                .setSocketTimeout(TIMEOUT)
                .setConnectTimeout(TIMEOUT)
                .setConnectionRequestTimeout(TIMEOUT)
                .build();

        CloseableHttpClient client = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();

        return new HttpComponentsClientHttpRequestFactory(client);
    }
}

Why not mocking using MockRestServiceServer with a RestTemplate, replaces the request factory. Therefore any RestTemplate settings will be replaced. Therefore using a real app for timeout testing might be the only option here.

Note: check also this article about RestTemplate configuration which include also the timeout configuration.