Description
Affected versions
org.springframework.cloud:spring-cloud-contract-stub-runner:4.2.1
org.springframework.cloud:spring-cloud-dependencies:2024.0.1
Describe the bug
When testing an application that uses a connection pool against the wiremock server, and the connection pool is reused between multiple @AutoConfigureWireMock(port = 0) test classes, then the first test method of the second test class will produce a NoHttpResponseException.
It seems that the version of wiremock and bundled jetty server that spring-cloud-contract uses does not close connections gracefully when the jetty server is restarted between each test class.
No such issue when using wiremock-spring-boot instead of spring-cloud-contract
If we configure wiremock using wiremock-spring-boot and @EnableWireMock instead of spring-cloud-contract the jetty server is not restarted on @afterclass and there is no such problem.
Issue hidden by automatic retries
In order to reliably reproduce the problem we need to disable automatic retries for the http client. Also we set wiremock.rest-template-ssl-enabled=false, otherwise our own http client will be overwritten and automatic retries enabled again.
The problem exists also with automatic retries, the http client still receives NoHttpResponseException, but will retry and then (usually) succeed. I guess unless the retry also grabs a connection from the pool that is available, but actually broken. This can happen in cases where we are doing requests from multiple threads against wiremock as part of our test.
DirtiesContext workaround
A workaround is to add the @DirtiesContext annotation to our test classes. However this will reduce performance of the test suite, potentially drastically reduce performance as well depending on the context startup time and number of tests.
Sample
Full sample: wiremock-broken-connections.zip
Application code
@Service
public class ExampleService {
private final Integer wireMockPort;
private final RestTemplate restTemplate;
public ExampleService(
@Value("${wiremock.server.port}") Integer wireMockPort,
RestTemplate restTemplate) {
this.wireMockPort = wireMockPort;
this.restTemplate = restTemplate;
}
public String getExternalEndpoint() {
return restTemplate.getForEntity("http://localhost:" + wireMockPort + "/external-endpoint", String.class)
.getBody();
}
}
// Rest template that disables automatic retries
@Configuration
public class RestTemplateConfiguration {
@Bean
public HttpComponentsClientHttpRequestFactoryBuilder httpComponentsClientHttpRequestFactoryBuilder() {
return ClientHttpRequestFactoryBuilder.httpComponents()
.withHttpClientCustomizer(HttpClientBuilder::disableAutomaticRetries);
}
@Bean
RestTemplate restTemplate(
RestTemplateBuilder restTemplateBuilder,
HttpComponentsClientHttpRequestFactoryBuilder requestFactoryBuilder) {
return restTemplateBuilder.requestFactoryBuilder(requestFactoryBuilder).build();
}
}
Two test classes that reproduce the issue:
@SpringBootTest(properties = "wiremock.rest-template-ssl-enabled=false")
@AutoConfigureWireMock(port = 0)
abstract class AbstractBaseTest {
@Autowired
ExampleService exampleService;
@BeforeEach
void setUpWireMock() {
WireMock.stubFor(WireMock.get("/external-endpoint")
.willReturn(aResponse().withBody("test-response").withStatus(HttpStatus.OK.value())));
}
@Test
void test1() {
exampleService.getExternalEndpoint();
}
@Test
void test2() {
exampleService.getExternalEndpoint();
}
}
class Test1 extends AbstractBaseTest {}
class Test2 extends AbstractBaseTest {}
These fail with the following exception:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:10494/external-endpoint": The target server failed to respond
at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:926)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:906)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:801)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:442)
at com.example.wiremock_broken_connections_mre.ExampleService.getExternalEndpoint(ExampleService.java:21)
at com.example.wiremock_broken_connections_mre.AbstractBaseTest.test1(Test.java:28)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.apache.hc.core5.http.NoHttpResponseException: The target server failed to respond
at org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:333)
at org.apache.hc.core5.http.impl.io.HttpRequestExecutor.execute(HttpRequestExecutor.java:196)
at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.lambda$execute$0(InternalExecRuntime.java:236)
at org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager$InternalConnectionEndpoint.execute(PoolingHttpClientConnectionManager.java:791)
at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.execute(InternalExecRuntime.java:233)
at org.apache.hc.client5.http.impl.classic.MainClientExec.execute(MainClientExec.java:120)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:198)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:150)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:110)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:174)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:87)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:55)
at org.apache.hc.client5.http.classic.HttpClient.executeOpen(HttpClient.java:183)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:99)
at org.springframework.http.client.AbstractStreamingClientHttpRequest.executeInternal(AbstractStreamingClientHttpRequest.java:71)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:81)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:900)
... 7 more