diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index ea631c65786..ce8e3c6bd3f 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -64,6 +64,7 @@ dependencies { testImplementation(testFixtures(project(":spring-web"))) testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.rometools:rome") + testImplementation("com.squareup.okhttp3:mockwebserver3") testImplementation("com.thoughtworks.xstream:xstream") testImplementation("de.bechte.junit:junit-hierarchicalcontextrunner") testImplementation("io.projectreactor.netty:reactor-netty-http") @@ -73,11 +74,10 @@ dependencies { testImplementation("jakarta.mail:jakarta.mail-api") testImplementation("jakarta.validation:jakarta.validation-api") testImplementation("javax.cache:cache-api") - testImplementation("org.apache.httpcomponents:httpclient") { - exclude group: "commons-logging", module: "commons-logging" - } + testImplementation("org.apache.httpcomponents.client5:httpclient5") testImplementation("org.awaitility:awaitility") testImplementation("org.easymock:easymock") + testImplementation("org.eclipse.jetty:jetty-reactive-httpclient") testImplementation("org.hibernate.orm:hibernate-core") testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.hsqldb:hsqldb") diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java index fb09ba3792c..2ab020d3b1c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java @@ -92,6 +92,8 @@ public class ExchangeResult { this.uriTemplate = uriTemplate; this.requestBody = requestBody; this.converterDelegate = converter; + // buffer response body in all cases, or connections might leak if expectations do not read the response + bufferResponseBody(); } ExchangeResult(ExchangeResult result) { @@ -241,6 +243,15 @@ public class ExchangeResult { formatBody(getResponseHeaders().getContentType(), getResponseBodyContent()) +"\n"; } + private void bufferResponseBody() { + try { + StreamUtils.drain(this.clientResponse.getBody()); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to get response content: " + ex); + } + } + private String formatStatus(HttpStatusCode statusCode) { String result = statusCode.toString(); if (statusCode instanceof HttpStatus status) { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientIntegrationTests.java new file mode 100644 index 00000000000..fb19b0882eb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/client/RestTestClientIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-present 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 + * + * https://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.test.web.servlet.client; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Function; +import java.util.stream.Stream; + +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.http.client.JettyClientHttpRequestFactory; +import org.springframework.http.client.ReactorClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; + +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +/** + * Integration tests for {@link RestTestClient} against a live server. + */ +class RestTestClientIntegrationTests { + + private RestTestClient client; + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @ParameterizedTest + @MethodSource("clientHttpRequestFactories") + @interface ParameterizedRestClientTest { + } + + static Stream clientHttpRequestFactories() { + return Stream.of( + argumentSet("JDK HttpURLConnection", new SimpleClientHttpRequestFactory()), + argumentSet("Jetty", new JettyClientHttpRequestFactory()), + argumentSet("JDK HttpClient", new JdkClientHttpRequestFactory()), + argumentSet("Reactor Netty", new ReactorClientHttpRequestFactory()), + argumentSet("HttpComponents", new HttpComponentsClientHttpRequestFactory()) + ); + } + + private MockWebServer server; + + private RestTestClient testClient; + + + private void startServer(ClientHttpRequestFactory requestFactory) throws IOException { + this.server = new MockWebServer(); + this.server.start(); + this.testClient = RestTestClient.bindToServer(requestFactory) + .baseUrl(this.server.url("/").toString()) + .build(); + } + + @ParameterizedRestClientTest // gh-35784 + void sequentialRequestsNotConsumingBody(ClientHttpRequestFactory requestFactory) throws IOException { + startServer(requestFactory); + for (int i = 0; i < 10; i++) { + prepareResponse(builder -> + builder.setHeader("Content-Type", "text/plain").body("Hello Spring!")); + this.testClient.get().uri("/").exchange().expectStatus().isOk(); + } + } + + private void prepareResponse(Function f) { + MockResponse.Builder builder = new MockResponse.Builder(); + this.server.enqueue(f.apply(builder).build()); + } + + @AfterEach + void shutdown() throws IOException { + if (server != null) { + this.server.close(); + } + } + +}