Skip to content

Commit 43f2de4

Browse files
committed
Defensive checks in WebClient and Reactor connector
Since there is no reason for an exchange to ever complete without a ClientResponse I've added a switchIfEmpty check at the WebClient level. Also, temporarily a second check closer to the problem in the ReactorClientHttpConnector suggesting a workaround and providing a reference to the Reactor Netty issue #138. Issue: SPR-15784
1 parent 5690358 commit 43f2de4

File tree

3 files changed

+42
-8
lines changed

3 files changed

+42
-8
lines changed

spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import reactor.core.publisher.Mono;
2424
import reactor.ipc.netty.http.client.HttpClient;
2525
import reactor.ipc.netty.http.client.HttpClientOptions;
26+
import reactor.ipc.netty.http.client.HttpClientRequest;
27+
import reactor.ipc.netty.http.client.HttpClientResponse;
2628
import reactor.ipc.netty.options.ClientOptions;
2729

2830
import org.springframework.http.HttpMethod;
@@ -36,6 +38,12 @@
3638
*/
3739
public class ReactorClientHttpConnector implements ClientHttpConnector {
3840

41+
private static final Mono<ClientHttpResponse> NO_CLIENT_RESPONSE_ERROR = Mono.error(
42+
new IllegalStateException("HttpClient completed without a response. " +
43+
"As a temporary workaround try to disable connection pool. " +
44+
"See https://github.com/reactor/reactor-netty/issues/138."));
45+
46+
3947
private final HttpClient httpClient;
4048

4149

@@ -61,11 +69,23 @@ public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
6169
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
6270

6371
return this.httpClient
64-
.request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()),
72+
.request(adaptHttpMethod(method),
6573
uri.toString(),
66-
httpClientRequest -> requestCallback
67-
.apply(new ReactorClientHttpRequest(method, uri, httpClientRequest)))
68-
.map(ReactorClientHttpResponse::new);
74+
request -> requestCallback.apply(adaptRequest(method, uri, request)))
75+
.map(this::adaptResponse)
76+
.switchIfEmpty(NO_CLIENT_RESPONSE_ERROR);
77+
}
78+
79+
private io.netty.handler.codec.http.HttpMethod adaptHttpMethod(HttpMethod method) {
80+
return io.netty.handler.codec.http.HttpMethod.valueOf(method.name());
81+
}
82+
83+
private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request) {
84+
return new ReactorClientHttpRequest(method, uri, request);
85+
}
86+
87+
private ClientHttpResponse adaptResponse(HttpClientResponse response) {
88+
return new ReactorClientHttpResponse(response);
6989
}
7090

7191
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
*/
6464
class DefaultWebClient implements WebClient {
6565

66+
private static final Mono<ClientResponse> NO_HTTP_CLIENT_RESPONSE_ERROR = Mono.error(
67+
new IllegalStateException("The underlying HTTP client completed without emitting a response."));
68+
69+
6670
private final ExchangeFunction exchangeFunction;
6771

6872
private final UriBuilderFactory uriBuilderFactory;
@@ -309,7 +313,7 @@ public Mono<ClientResponse> exchange() {
309313
ClientRequest request = (this.inserter != null ?
310314
initRequestBuilder().body(this.inserter).build() :
311315
initRequestBuilder().build());
312-
return exchangeFunction.exchange(request);
316+
return exchangeFunction.exchange(request).switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
313317
}
314318

315319
private ClientRequest.Builder initRequestBuilder() {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.reactive.function.client;
1818

19+
import java.time.Duration;
1920
import java.util.Collections;
2021

2122
import org.junit.Before;
@@ -25,12 +26,15 @@
2526
import org.mockito.Mockito;
2627
import org.mockito.MockitoAnnotations;
2728
import reactor.core.publisher.Mono;
29+
import reactor.test.StepVerifier;
2830

2931
import org.springframework.http.HttpHeaders;
3032
import org.springframework.http.MediaType;
3133

32-
import static org.junit.Assert.*;
33-
import static org.mockito.Mockito.*;
34+
import static org.junit.Assert.assertEquals;
35+
import static org.mockito.Mockito.mock;
36+
import static org.mockito.Mockito.verifyNoMoreInteractions;
37+
import static org.mockito.Mockito.when;
3438

3539
/**
3640
* Unit tests for {@link DefaultWebClient}.
@@ -160,7 +164,7 @@ public void attributes() {
160164
@Test
161165
public void apply() {
162166
WebClient client = builder()
163-
.apply(builder -> builder.defaultHeader("Accept", "application/json").defaultCookie("id", "123"))
167+
.apply(builder -> builder.defaultHeader("Accept", "application/json").defaultCookie("id", "123"))
164168
.build();
165169
client.get().uri("/path").exchange();
166170

@@ -170,6 +174,12 @@ public void apply() {
170174
verifyNoMoreInteractions(this.exchangeFunction);
171175
}
172176

177+
@Test
178+
public void switchToErrorOnEmptyClientResponseMono() throws Exception {
179+
StepVerifier.create(builder().build().get().uri("/path").exchange())
180+
.expectErrorMessage("The underlying HTTP client completed without emitting a response.")
181+
.verify(Duration.ofSeconds(5));
182+
}
173183

174184

175185
private WebClient.Builder builder() {

0 commit comments

Comments
 (0)