diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index 35a0dbab2d..a5d8c0e181 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -3,7 +3,7 @@ image::https://badges.gitter.im/Join%20Chat.svg[Gitter, link="https://gitter.im/ image::https://codecov.io/gh/spring-cloud/spring-cloud-contract/branch/{branch}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-contract"] image::https://circleci.com/gh/spring-cloud/spring-cloud-contract.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-contract"] :introduction_url: ../../../.. -:verifier_core_path: ../../../../spring-cloud-contract-verifier +:verifier_core_path: {introduction_url}/spring-cloud-contract-verifier == Spring Cloud Contract diff --git a/docs/src/main/asciidoc/verifier_faq.adoc b/docs/src/main/asciidoc/verifier_faq.adoc index c8ba0c965b..7924df0f1f 100644 --- a/docs/src/main/asciidoc/verifier_faq.adoc +++ b/docs/src/main/asciidoc/verifier_faq.adoc @@ -877,3 +877,39 @@ was started will be attached. Yes! With version 1.2.0 we've added such a possibility. It's enough to call `file(...)` method in the DSL and provide a path relative to where the contract lays. If you're using YAML just use the `bodyFromFile` property. + +==== Why do I sometimes get `SocketException` + +When using an HTTP client (e.g. `RestTemplate`) and running several tests that share a Spring context, you might sometimes get the following exception: + +``` +java.net.SocketException: Unexpected end of file from server +``` + +Tom Akehurst, the creator of WireMock did the following analysis of this issue. + +> I looked at tcpdump while running the failing test. `HttpUrlConnection` is doing something weird - it's creating a connection in a previous test case, which works fine, then the usual `fin` -> `fin ack` etc. ending handshake happens. But it seems it isn't discarded, but reused after that. Because the server thinks (rightly) that the connection is closed, it just sends a RST packet. Calling the `/__admin` endpoint just happened to remove the dead connection from the pool. This also fixes the problem (which using the Java HTTP client): System.setProperty("http.keepAlive", "false"); + +There are the ways to solve this problem. + +First, just use a different HTTP client for `RestTemplate`. Example for using Apache HTTP client: + +.pom.xml +[source,xml,indent=0] +---- +include::{standalone_restdocs_path}/http-client/pom.xml[tags=httpclient,indent=0] +---- + +[source,java,indent=0] +---- +include::{standalone_restdocs_path}/http-client/src/main/java/com/example/loan/LoanApplicationService.java[tags=custom_request_factory,indent=0] +---- + +Second option is to set the system property. You can set it either in code or pass it to your tests via a plugin. + +[source,java,indent=0] +---- +static { + System.setProperty("http.keepAlive", "false"); +} +---- \ No newline at end of file diff --git a/samples/standalone/restdocs/http-client/pom.xml b/samples/standalone/restdocs/http-client/pom.xml index 6563fd4f18..77ab4a9a0f 100644 --- a/samples/standalone/restdocs/http-client/pom.xml +++ b/samples/standalone/restdocs/http-client/pom.xml @@ -1,4 +1,21 @@ + + 4.0.0 @@ -53,6 +70,13 @@ spring-cloud-contract-stub-runner test + + + org.apache.httpcomponents + httpclient + compile + + com.example http-server-restdocs diff --git a/samples/standalone/restdocs/http-client/src/main/java/com/example/loan/LoanApplicationService.java b/samples/standalone/restdocs/http-client/src/main/java/com/example/loan/LoanApplicationService.java index 6c5d0861f5..9f3c759b9e 100644 --- a/samples/standalone/restdocs/http-client/src/main/java/com/example/loan/LoanApplicationService.java +++ b/samples/standalone/restdocs/http-client/src/main/java/com/example/loan/LoanApplicationService.java @@ -1,12 +1,21 @@ -package com.example.loan; +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +package com.example.loan; import com.example.loan.model.FraudCheckStatus; import com.example.loan.model.FraudServiceRequest; @@ -15,6 +24,15 @@ import com.example.loan.model.LoanApplication; import com.example.loan.model.LoanApplicationResult; import com.example.loan.model.LoanApplicationStatus; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + @Service @ConfigurationProperties("service") public class LoanApplicationService { @@ -28,6 +46,10 @@ public class LoanApplicationService { public LoanApplicationService() { this.restTemplate = new RestTemplate(); + // tag::custom_request_factory[] + this.restTemplate + .setRequestFactory(new HttpComponentsClientHttpRequestFactory()); + // end::custom_request_factory[] } public LoanApplicationResult loanApplication(LoanApplication loanApplication) { diff --git a/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/XmlServiceTests.java b/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/XmlServiceTests.java index 351ce62021..77e0b2682b 100644 --- a/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/XmlServiceTests.java +++ b/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/XmlServiceTests.java @@ -1,3 +1,20 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package com.example.loan; import java.net.URI; @@ -8,6 +25,7 @@ import com.github.tomakehurst.wiremock.stubbing.StubMapping; import org.assertj.core.api.BDDAssertions; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; diff --git a/spring-cloud-contract-starters/spring-cloud-starter-contract-stub-runner/pom.xml b/spring-cloud-contract-starters/spring-cloud-starter-contract-stub-runner/pom.xml index 29afdc9718..8e416bbe90 100644 --- a/spring-cloud-contract-starters/spring-cloud-starter-contract-stub-runner/pom.xml +++ b/spring-cloud-contract-starters/spring-cloud-starter-contract-stub-runner/pom.xml @@ -1,4 +1,21 @@ + + 4.0.0 @@ -20,6 +37,7 @@ org.springframework.boot spring-boot-starter-web + true org.springframework.boot diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/StubRunnerWireMockTestExecutionListener.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/StubRunnerWireMockTestExecutionListener.java index aecbfb0b21..8b777c471d 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/StubRunnerWireMockTestExecutionListener.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/StubRunnerWireMockTestExecutionListener.java @@ -1,3 +1,20 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package org.springframework.cloud.contract.stubrunner.provider.wiremock; import java.util.List; @@ -7,12 +24,13 @@ import java.util.concurrent.ConcurrentHashMap; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.contract.stubrunner.HttpServerStub; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.cloud.contract.wiremock.WireMockUtils; import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; -import org.springframework.util.Assert; -import org.springframework.web.client.RestTemplate; /** * Stops the {@link HttpServerStub} after each test class @@ -30,6 +48,12 @@ public final class StubRunnerWireMockTestExecutionListener @Override public void beforeTestClass(TestContext testContext) { + if (testContext.getTestClass().getAnnotationsByType(AutoConfigureStubRunner.class).length == 0) { + if (log.isTraceEnabled()) { + log.trace("No @AutoConfigureStubRunner annotation found on [" + testContext.getTestClass() + "]. Skipping"); + } + return; + } Map stubs = STUBS .get(testContext.getApplicationContext()); if (stubs != null) { @@ -45,34 +69,24 @@ public final class StubRunnerWireMockTestExecutionListener List mappings = entry.getValue().mappings; if (log.isDebugEnabled()) { log.debug("Stopped a running WireMock instance at " + "port [" - + entry.getValue().port + "] with stub mappings " + "size [" + + entry.getValue().port + "] with stub mappings size [" + mappings.size() + "]. Restarting the stub."); } entry.getKey().start(entry.getValue().port); entry.getKey().registerDescriptors(mappings); - /* - * Thanks to Tom Akehurst: I looked at tcpdump while running the failing - * test. HttpUrlConnection is doing something weird - it's creating a - * connection in a previous test case, which works fine, then the usual - * fin -> fin ack etc. etc. ending handshake happens. But it seems it - * isn't discarded, but reused after that. Because the server thinks - * (rightly) that the connection is closed, it just sends a RST packet. - * Calling the admin endpoint just happened to remove the dead connection - * from the pool. This also fixes the problem (which using the Java HTTP - * client): System.setProperty("http.keepAlive", "false"); - */ - Assert.isTrue( - new RestTemplate() - .getForEntity("http://localhost:" + entry.getValue().port - + "/__admin/mappings", String.class) - .getStatusCode().is2xxSuccessful(), - "__admin/mappings endpoint wasn't accessible"); + WireMockUtils.getMappingsEndpoint(entry.getValue().port); } } } @Override public void afterTestClass(TestContext testContext) { + if (testContext.getTestClass().getAnnotationsByType(AutoConfigureStubRunner.class).length == 0) { + if (log.isTraceEnabled()) { + log.trace("No @AutoConfigureStubRunner annotation found on [" + testContext.getTestClass() + "]. Skipping"); + } + return; + } STUBS.put(testContext.getApplicationContext(), WireMockHttpServerStub.SERVERS); if (log.isDebugEnabled()) { log.debug("Stopping servers " + WireMockHttpServerStub.SERVERS); @@ -81,5 +95,4 @@ public final class StubRunnerWireMockTestExecutionListener serverStub.stop(); } } - } \ No newline at end of file diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java index c13290a9dc..5eba4c3cd5 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java @@ -1,3 +1,20 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package org.springframework.cloud.contract.stubrunner.provider.wiremock; import java.io.File; @@ -20,6 +37,8 @@ import com.github.tomakehurst.wiremock.extension.Extension; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import wiremock.com.github.jknack.handlebars.Helper; + import org.springframework.cloud.contract.stubrunner.HttpServerStub; import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper; import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper; @@ -31,7 +50,6 @@ import org.springframework.util.ClassUtils; import org.springframework.util.SocketUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; -import wiremock.com.github.jknack.handlebars.Helper; /** * Abstraction over WireMock as a HTTP Server Stub @@ -115,7 +133,7 @@ public class WireMockHttpServerStub implements HttpServerStub { log.debug("Started WireMock at port [" + port + "]"); } if (!SERVERS.containsKey(this)) { - SERVERS.put(this, new PortAndMappings(port, new ArrayList())); + SERVERS.put(this, new PortAndMappings(port, new ArrayList<>())); } return this; } diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java index 14b0ce135c..0e558992f2 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java @@ -1,17 +1,18 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2013-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package org.springframework.cloud.contract.wiremock; @@ -20,6 +21,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; + import javax.annotation.PostConstruct; import com.github.tomakehurst.wiremock.WireMockServer; @@ -28,6 +30,7 @@ import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.github.tomakehurst.wiremock.core.Options; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -38,10 +41,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; /** * Configuration and lifecycle for a Spring Application context that wants to run a @@ -84,8 +85,7 @@ public class WireMockConfiguration implements SmartLifecycle { @PostConstruct public void init() throws IOException { if (this.options == null) { - com.github.tomakehurst.wiremock.core.WireMockConfiguration factory = WireMockSpring - .options(); + com.github.tomakehurst.wiremock.core.WireMockConfiguration factory = WireMockSpring.options(); if (this.wireMock.getServer().getPort() != 8080) { factory.port(this.wireMock.getServer().getPort()); } @@ -105,7 +105,7 @@ public class WireMockConfiguration implements SmartLifecycle { registerStubs(); if (log.isDebugEnabled()) { log.debug("WireMock server has [" + this.server.getStubMappings().size() - + "] registered"); + + "] stubs registered"); } if (!this.beanFactory.containsBean(WIREMOCK_SERVER_BEAN_NAME)) { this.beanFactory.registerSingleton(WIREMOCK_SERVER_BEAN_NAME, this.server); @@ -133,6 +133,10 @@ public class WireMockConfiguration implements SmartLifecycle { } } + int port() { + return this.server.port(); + } + void reset() { this.server.resetAll(); } @@ -168,28 +172,14 @@ public class WireMockConfiguration implements SmartLifecycle { log.debug("Started WireMock at port [" + this.server.port() + "]. It has [" + this.server.getStubMappings().size() + "] mappings registered"); } - /* - * Thanks to Tom Akehurst: I looked at tcpdump while running the failing test. - * HttpUrlConnection is doing something weird - it's creating a connection in a - * previous test case, which works fine, then the usual fin -> fin ack etc. etc. - * ending handshake happens. But it seems it isn't discarded, but reused after - * that. Because the server thinks (rightly) that the connection is closed, it - * just sends a RST packet. Calling the admin endpoint just happened to remove the - * dead connection from the pool. This also fixes the problem (which using the - * Java HTTP client): System.setProperty("http.keepAlive", "false"); - */ - Assert.isTrue( - new RestTemplate() - .getForEntity("http://localhost:" + this.server.port() - + "/__admin/mappings", String.class) - .getStatusCode().is2xxSuccessful(), - "__admin/mappings endpoint wasn't accessible"); + WireMockUtils.getMappingsEndpoint(this.port()); } @Override public void stop() { if (this.running) { - this.server.stop(); + reset(); + this.server.shutdownServer(); this.running = false; if (log.isDebugEnabled()) { log.debug("Stopped WireMock instance"); diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockTestExecutionListener.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockTestExecutionListener.java index 771121dc75..07b4f76d20 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockTestExecutionListener.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockTestExecutionListener.java @@ -1,7 +1,25 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package org.springframework.cloud.contract.wiremock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -28,9 +46,9 @@ public final class WireMockTestExecutionListener extends AbstractTestExecutionLi + wireMockConfiguration.isRunning() + "]"); } if (!wireMockConfiguration.isRunning()) { - wireMockConfiguration.reset(); wireMockConfiguration.init(); wireMockConfiguration.start(); + WireMockUtils.getMappingsEndpoint(wireMockConfiguration.port()); } } catch (Exception e) { diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockUtils.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockUtils.java new file mode 100644 index 0000000000..5b700ff9c8 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.contract.wiremock; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +import org.springframework.util.Assert; + +/** + * Utility class to work with WireMock. + * + * @author Marcin Grzejszczak + * @since 2.0.3 + */ +public final class WireMockUtils { + + private WireMockUtils() { + throw new IllegalStateException("Don't instantiate"); + } + + /** + * Thanks to Tom Akehurst: I looked at tcpdump while running the failing + * test. HttpUrlConnection is doing something weird - it's creating a + * connection in a previous test case, which works fine, then the usual + * fin -> fin ack etc. etc. ending handshake happens. But it seems it + * isn't discarded, but reused after that. Because the server thinks + * (rightly) that the connection is closed, it just sends a RST packet. + * Calling the admin endpoint just happened to remove the dead connection + * from the pool. This also fixes the problem (which using the Java HTTP + * client): System.setProperty("http.keepAlive", "false"); + **/ + public static CloseableHttpResponse getMappingsEndpoint(int port) { + CloseableHttpClient client = HttpClientBuilder.create().build(); + try { + CloseableHttpResponse response = client + .execute(new HttpHost("localhost", port), new HttpGet("/__admin/mappings")); + Assert.isTrue(response.getStatusLine().getStatusCode() == 200, "Status code must be 200"); + return response; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } +}