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.
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.