14

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.

  • are you able to connect without configuring timeout? – Vaibs May 11 '17 at 08:15
  • @ Vaibs, No. for testing purpose i did not start external web service. Specified time out is not working. – Easy2DownVoteHard2Ans May 11 '17 at 08:55
  • Just tested with your code in my environment. Its working fine.Your service should up to test this. To achieve this ,my suggestion is just minimise the connection timeout value . change to 10ms. – Vaibs May 11 '17 at 09:14
  • 1
    @ Vaibs, when external service is up, setReadTimeout is working. when external service is down, setConnectTimeout is not working. that is, getting exception after a second it is not waiting for 10 second which i configure. – Easy2DownVoteHard2Ans May 11 '17 at 09:28
  • @ Vaibs, even when i try 10 or 5 ms it is not working. i got successful response from external server. – Easy2DownVoteHard2Ans May 11 '17 at 10:07
  • 1
    If your service is down why rest templte poll for 10 sec.It will directly throw some exception/error – Vaibs May 11 '17 at 10:19
  • @ Vaibs, no i tried with server up with 10 and 5 ms - not working. i tried with server down with 5 sec and 10 sec thrown exception within 1 sec. – Easy2DownVoteHard2Ans May 11 '17 at 10:22
  • 2
    @Easy2DownVoteHard2Ans There are two scenarios: 1) the remote server is up but it took longer than `connectTimeout` to get a connection and 2) the server is down and therefore unreachable. For the former the `connectTimeout` should work, for latter it wouldn't make sense because your network client already knows is unreachable and it wouldn't make sense to make you wait up to `comnectTimeout` to tell you so. – Edwin Dalorzo May 11 '17 at 16:41
  • @EdwinDalorzo. Thank you for your comment. i have tested with 10.255.255.1 and conncection timeout is working as expected, – Easy2DownVoteHard2Ans May 15 '17 at 05:14

1 Answers1

46

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.

Community
  • 1
  • 1
andreim
  • 3,365
  • 23
  • 21
  • 1
    Thank you for the great, inclusive and detailed answer. This is a very helpful guide for dealing with and testing connection timeout issues with Spring. – Danny Bullis Jul 19 '18 at 18:37
  • 1
    nice answer. is setSocketTimeout (in the last RequestConfig version) equivalent to setReadTimeout (1st version) ? – Chris Jan 27 '20 at 09:59