Skip to content

Commit 9bd989f

Browse files
ascopesrstoyanchev
authored andcommitted
WebClient tests for socket and response format issues
Added test case for malformed response chunk, which is now failing as expected. See gh-27262
1 parent d5597a7 commit 9bd989f

File tree

1 file changed

+142
-3
lines changed

1 file changed

+142
-3
lines changed

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.OutputStream;
2123
import java.io.UncheckedIOException;
2224
import java.lang.annotation.ElementType;
2325
import java.lang.annotation.Retention;
2426
import java.lang.annotation.RetentionPolicy;
2527
import java.lang.annotation.Target;
28+
import java.net.ServerSocket;
29+
import java.net.Socket;
2630
import java.net.URI;
2731
import java.nio.charset.StandardCharsets;
2832
import java.nio.file.Files;
@@ -31,16 +35,21 @@
3135
import java.util.List;
3236
import java.util.Map;
3337
import java.util.function.Consumer;
38+
import java.util.function.Function;
3439
import java.util.stream.Collectors;
3540
import java.util.stream.Stream;
3641

3742
import okhttp3.mockwebserver.MockResponse;
3843
import okhttp3.mockwebserver.MockWebServer;
3944
import okhttp3.mockwebserver.RecordedRequest;
45+
import okhttp3.mockwebserver.SocketPolicy;
4046
import org.junit.jupiter.api.AfterEach;
4147
import org.junit.jupiter.api.Test;
4248
import org.junit.jupiter.params.ParameterizedTest;
49+
import org.junit.jupiter.params.provider.Arguments;
4350
import org.junit.jupiter.params.provider.MethodSource;
51+
import org.springframework.util.SocketUtils;
52+
import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
4453
import reactor.core.publisher.Flux;
4554
import reactor.core.publisher.Mono;
4655
import reactor.netty.http.client.HttpClient;
@@ -83,7 +92,7 @@ class WebClientIntegrationTests {
8392

8493
@Retention(RetentionPolicy.RUNTIME)
8594
@Target(ElementType.METHOD)
86-
@ParameterizedTest(name = "[{index}] webClient [{0}]")
95+
@ParameterizedTest(name = "[{index}] {displayName} [{0}]")
8796
@MethodSource("arguments")
8897
@interface ParameterizedWebClientTest {
8998
}
@@ -113,7 +122,9 @@ private void startServer(ClientHttpConnector connector) {
113122

114123
@AfterEach
115124
void shutdown() throws IOException {
116-
this.server.shutdown();
125+
if (server != null) {
126+
this.server.shutdown();
127+
}
117128
}
118129

119130

@@ -1209,6 +1220,135 @@ void invalidDomain(ClientHttpConnector connector) {
12091220
.verify();
12101221
}
12111222

1223+
static Stream<Arguments> socketFaultArguments() {
1224+
Stream.Builder<Arguments> argumentsBuilder = Stream.builder();
1225+
arguments().forEach(arg -> {
1226+
argumentsBuilder.accept(Arguments.of(arg, SocketPolicy.DISCONNECT_AT_START));
1227+
argumentsBuilder.accept(Arguments.of(arg, SocketPolicy.DISCONNECT_DURING_REQUEST_BODY));
1228+
argumentsBuilder.accept(Arguments.of(arg, SocketPolicy.DISCONNECT_AFTER_REQUEST));
1229+
});
1230+
return argumentsBuilder.build();
1231+
}
1232+
1233+
@ParameterizedTest(name = "[{index}] {displayName} [{0}, {1}]")
1234+
@MethodSource("socketFaultArguments")
1235+
void prematureClosureFault(ClientHttpConnector connector, SocketPolicy socketPolicy) {
1236+
startServer(connector);
1237+
1238+
prepareResponse(response -> response
1239+
.setSocketPolicy(socketPolicy)
1240+
.setStatus("HTTP/1.1 200 OK")
1241+
.setHeader("Response-Header-1", "value 1")
1242+
.setHeader("Response-Header-2", "value 2")
1243+
.setBody("{\"message\": \"Hello, World!\"}"));
1244+
1245+
String uri = "/test";
1246+
Mono<String> result = this.webClient
1247+
.post()
1248+
.uri(uri)
1249+
// Random non-empty body to allow us to interrupt.
1250+
.bodyValue("{\"action\": \"Say hello!\"}")
1251+
.retrieve()
1252+
.bodyToMono(String.class);
1253+
1254+
StepVerifier.create(result)
1255+
.expectErrorSatisfies(throwable -> {
1256+
assertThat(throwable).isInstanceOf(WebClientRequestException.class);
1257+
WebClientRequestException ex = (WebClientRequestException) throwable;
1258+
// Varies between connector providers.
1259+
assertThat(ex.getCause()).isInstanceOf(IOException.class);
1260+
})
1261+
.verify();
1262+
}
1263+
1264+
static Stream<Arguments> malformedResponseChunkArguments() {
1265+
return Stream.of(
1266+
Arguments.of(new ReactorClientHttpConnector(), true),
1267+
Arguments.of(new JettyClientHttpConnector(), true),
1268+
// Apache injects the Transfer-Encoding header for us, and complains with an exception if we also
1269+
// add it. The other two connectors do not add the header at all. We need this header for the test
1270+
// case to work correctly.
1271+
Arguments.of(new HttpComponentsClientHttpConnector(), false)
1272+
);
1273+
}
1274+
1275+
@ParameterizedTest(name = "[{index}] {displayName} [{0}, {1}]")
1276+
@MethodSource("malformedResponseChunkArguments")
1277+
void malformedResponseChunksOnBodilessEntity(ClientHttpConnector connector, boolean addTransferEncodingHeader) {
1278+
Mono<?> result = doMalformedResponseChunks(connector, addTransferEncodingHeader, ResponseSpec::toBodilessEntity);
1279+
1280+
StepVerifier.create(result)
1281+
.expectErrorSatisfies(throwable -> {
1282+
assertThat(throwable).isInstanceOf(WebClientException.class);
1283+
WebClientException ex = (WebClientException) throwable;
1284+
assertThat(ex.getCause()).isInstanceOf(IOException.class);
1285+
})
1286+
.verify();
1287+
}
1288+
1289+
@ParameterizedTest(name = "[{index}] {displayName} [{0}, {1}]")
1290+
@MethodSource("malformedResponseChunkArguments")
1291+
void malformedResponseChunksOnEntityWithBody(ClientHttpConnector connector, boolean addTransferEncodingHeader) {
1292+
Mono<?> result = doMalformedResponseChunks(connector, addTransferEncodingHeader, spec -> spec.toEntity(String.class));
1293+
1294+
StepVerifier.create(result)
1295+
.expectErrorSatisfies(throwable -> {
1296+
assertThat(throwable).isInstanceOf(WebClientException.class);
1297+
WebClientException ex = (WebClientException) throwable;
1298+
assertThat(ex.getCause()).isInstanceOf(IOException.class);
1299+
})
1300+
.verify();
1301+
}
1302+
1303+
private <T> Mono<T> doMalformedResponseChunks(
1304+
ClientHttpConnector connector,
1305+
boolean addTransferEncodingHeader,
1306+
Function<ResponseSpec, Mono<T>> responseHandler
1307+
) {
1308+
int port = SocketUtils.findAvailableTcpPort();
1309+
1310+
Thread serverThread = new Thread(() -> {
1311+
// This exists separately to the main mock server, as I had a really hard time getting that to send the
1312+
// chunked responses correctly, flushing the socket each time. This was the only way I was able to replicate
1313+
// the issue of the client not handling malformed response chunks correctly.
1314+
try (ServerSocket serverSocket = new ServerSocket(port)) {
1315+
Socket socket = serverSocket.accept();
1316+
InputStream is = socket.getInputStream();
1317+
1318+
//noinspection ResultOfMethodCallIgnored
1319+
is.read(new byte[4096]);
1320+
1321+
OutputStream os = socket.getOutputStream();
1322+
os.write("HTTP/1.1 200 OK\r\n".getBytes(StandardCharsets.UTF_8));
1323+
os.write("Transfer-Encoding: chunked\r\n".getBytes(StandardCharsets.UTF_8));
1324+
os.write("\r\n".getBytes(StandardCharsets.UTF_8));
1325+
os.write("lskdu018973t09sylgasjkfg1][]'./.sdlv".getBytes(StandardCharsets.UTF_8));
1326+
socket.close();
1327+
} catch (IOException ex) {
1328+
throw new RuntimeException(ex);
1329+
}
1330+
});
1331+
1332+
serverThread.setDaemon(true);
1333+
serverThread.start();
1334+
1335+
ResponseSpec spec = WebClient
1336+
.builder()
1337+
.clientConnector(connector)
1338+
.baseUrl("http://localhost:" + port)
1339+
.build()
1340+
.post()
1341+
.headers(headers -> {
1342+
if (addTransferEncodingHeader) {
1343+
headers.add(HttpHeaders.TRANSFER_ENCODING, "chunked");
1344+
}
1345+
})
1346+
.retrieve();
1347+
1348+
return responseHandler
1349+
.apply(spec)
1350+
.doFinally(signal -> serverThread.stop());
1351+
}
12121352

12131353
private void prepareResponse(Consumer<MockResponse> consumer) {
12141354
MockResponse response = new MockResponse();
@@ -1252,5 +1392,4 @@ public void setContainerValue(T containerValue) {
12521392
this.containerValue = containerValue;
12531393
}
12541394
}
1255-
12561395
}

0 commit comments

Comments
 (0)