Skip to content

Commit 08e6d76

Browse files
committed
Always buffer response body in RestTestClient
Prior to this commit, `RestTestClient` tests could only perform expectations on the response without consuming the body. In this case, the client could leak HTTP connections with the underlying HTTP library because the response was not entirely read. This commit ensures that the response is always fully drained before performing expectations. The client is configured to buffer the response content, so further body expectations are always possible. Fixes gh-35784
1 parent 7a19cbb commit 08e6d76

File tree

3 files changed

+116
-3
lines changed

3 files changed

+116
-3
lines changed

spring-test/spring-test.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies {
6464
testImplementation(testFixtures(project(":spring-web")))
6565
testImplementation("com.fasterxml.jackson.core:jackson-databind")
6666
testImplementation("com.rometools:rome")
67+
testImplementation("com.squareup.okhttp3:mockwebserver3")
6768
testImplementation("com.thoughtworks.xstream:xstream")
6869
testImplementation("de.bechte.junit:junit-hierarchicalcontextrunner")
6970
testImplementation("io.projectreactor.netty:reactor-netty-http")
@@ -73,11 +74,10 @@ dependencies {
7374
testImplementation("jakarta.mail:jakarta.mail-api")
7475
testImplementation("jakarta.validation:jakarta.validation-api")
7576
testImplementation("javax.cache:cache-api")
76-
testImplementation("org.apache.httpcomponents:httpclient") {
77-
exclude group: "commons-logging", module: "commons-logging"
78-
}
77+
testImplementation("org.apache.httpcomponents.client5:httpclient5")
7978
testImplementation("org.awaitility:awaitility")
8079
testImplementation("org.easymock:easymock")
80+
testImplementation("org.eclipse.jetty:jetty-reactive-httpclient")
8181
testImplementation("org.hibernate.orm:hibernate-core")
8282
testImplementation("org.hibernate.validator:hibernate-validator")
8383
testImplementation("org.hsqldb:hsqldb")

spring-test/src/main/java/org/springframework/test/web/servlet/client/ExchangeResult.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public class ExchangeResult {
9292
this.uriTemplate = uriTemplate;
9393
this.requestBody = requestBody;
9494
this.converterDelegate = converter;
95+
// buffer response body in all cases, or connections might leak if expectations do not read the response
96+
bufferResponseBody();
9597
}
9698

9799
ExchangeResult(ExchangeResult result) {
@@ -241,6 +243,15 @@ public String toString() {
241243
formatBody(getResponseHeaders().getContentType(), getResponseBodyContent()) +"\n";
242244
}
243245

246+
private void bufferResponseBody() {
247+
try {
248+
StreamUtils.drain(this.clientResponse.getBody());
249+
}
250+
catch (IOException ex) {
251+
throw new IllegalStateException("Failed to get response content: " + ex);
252+
}
253+
}
254+
244255
private String formatStatus(HttpStatusCode statusCode) {
245256
String result = statusCode.toString();
246257
if (statusCode instanceof HttpStatus status) {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.web.servlet.client;
18+
19+
import java.io.IOException;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.util.function.Function;
25+
import java.util.stream.Stream;
26+
27+
import mockwebserver3.MockResponse;
28+
import mockwebserver3.MockWebServer;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.params.ParameterizedTest;
31+
import org.junit.jupiter.params.provider.Arguments;
32+
import org.junit.jupiter.params.provider.MethodSource;
33+
34+
import org.springframework.http.client.ClientHttpRequestFactory;
35+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
36+
import org.springframework.http.client.JdkClientHttpRequestFactory;
37+
import org.springframework.http.client.JettyClientHttpRequestFactory;
38+
import org.springframework.http.client.ReactorClientHttpRequestFactory;
39+
import org.springframework.http.client.SimpleClientHttpRequestFactory;
40+
41+
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
42+
43+
/**
44+
* Integration tests for {@link RestTestClient} against a live server.
45+
*/
46+
class RestTestClientIntegrationTests {
47+
48+
private RestTestClient client;
49+
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@Target(ElementType.METHOD)
52+
@ParameterizedTest
53+
@MethodSource("clientHttpRequestFactories")
54+
@interface ParameterizedRestClientTest {
55+
}
56+
57+
static Stream<Arguments> clientHttpRequestFactories() {
58+
return Stream.of(
59+
argumentSet("JDK HttpURLConnection", new SimpleClientHttpRequestFactory()),
60+
argumentSet("Jetty", new JettyClientHttpRequestFactory()),
61+
argumentSet("JDK HttpClient", new JdkClientHttpRequestFactory()),
62+
argumentSet("Reactor Netty", new ReactorClientHttpRequestFactory()),
63+
argumentSet("HttpComponents", new HttpComponentsClientHttpRequestFactory())
64+
);
65+
}
66+
67+
private MockWebServer server;
68+
69+
private RestTestClient testClient;
70+
71+
72+
private void startServer(ClientHttpRequestFactory requestFactory) throws IOException {
73+
this.server = new MockWebServer();
74+
this.server.start();
75+
this.testClient = RestTestClient.bindToServer(requestFactory)
76+
.baseUrl(this.server.url("/").toString())
77+
.build();
78+
}
79+
80+
@ParameterizedRestClientTest // gh-35784
81+
void sequentialRequestsNotConsumingBody(ClientHttpRequestFactory requestFactory) throws IOException {
82+
startServer(requestFactory);
83+
for (int i = 0; i < 10; i++) {
84+
prepareResponse(builder ->
85+
builder.setHeader("Content-Type", "text/plain").body("Hello Spring!"));
86+
this.testClient.get().uri("/").exchange().expectStatus().isOk();
87+
}
88+
}
89+
90+
private void prepareResponse(Function<MockResponse.Builder, MockResponse.Builder> f) {
91+
MockResponse.Builder builder = new MockResponse.Builder();
92+
this.server.enqueue(f.apply(builder).build());
93+
}
94+
95+
@AfterEach
96+
void shutdown() throws IOException {
97+
if (server != null) {
98+
this.server.close();
99+
}
100+
}
101+
102+
}

0 commit comments

Comments
 (0)