Skip to content

Commit 74f64c4

Browse files
committed
Wrap exceptions in WebClient
This commit makes sure that exceptions emitted by WebClient are wrapped by WebClientExceptions: - Exceptions emitted by the ClientHttpConnector are wrapped in a new WebClientRequestException. - Exceptions emitted after a response is received are wrapped in a WebClientResponseException Closes gh-23842
1 parent 4dfecde commit 74f64c4

File tree

7 files changed

+131
-8
lines changed

7 files changed

+131
-8
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
*/
5656
class DefaultClientResponse implements ClientResponse {
5757

58+
private static final byte[] EMPTY = new byte[0];
59+
60+
5861
private final ClientHttpResponse response;
5962

6063
private final Headers headers;
@@ -200,7 +203,8 @@ public Mono<WebClientResponseException> createException() {
200203
DataBufferUtils.release(dataBuffer);
201204
return bytes;
202205
})
203-
.defaultIfEmpty(new byte[0])
206+
.defaultIfEmpty(EMPTY)
207+
.onErrorReturn(IllegalStateException.class::isInstance, EMPTY)
204208
.map(bodyBytes -> {
205209
HttpRequest request = this.requestSupplier.get();
206210
Charset charset = headers().contentType()

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

+14-4
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,13 @@ public <T> Mono<T> bodyToMono(ParameterizedTypeReference<T> elementTypeRef) {
488488

489489
private <T> Mono<T> handleBodyMono(ClientResponse response, Mono<T> bodyPublisher) {
490490
Mono<T> result = statusHandlers(response);
491+
Mono<T> wrappedExceptions = bodyPublisher.onErrorResume(WebClientUtils::shouldWrapException,
492+
t -> wrapException(t, response));
491493
if (result != null) {
492-
return result.switchIfEmpty(bodyPublisher);
494+
return result.switchIfEmpty(wrappedExceptions);
493495
}
494496
else {
495-
return bodyPublisher;
497+
return wrappedExceptions;
496498
}
497499
}
498500

@@ -510,11 +512,13 @@ public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef) {
510512

511513
private <T> Publisher<T> handleBodyFlux(ClientResponse response, Flux<T> bodyPublisher) {
512514
Mono<T> result = statusHandlers(response);
515+
Flux<T> wrappedExceptions = bodyPublisher.onErrorResume(WebClientUtils::shouldWrapException,
516+
t -> wrapException(t, response));
513517
if (result != null) {
514-
return result.flux().switchIfEmpty(bodyPublisher);
518+
return result.flux().switchIfEmpty(wrappedExceptions);
515519
}
516520
else {
517-
return bodyPublisher;
521+
return wrappedExceptions;
518522
}
519523
}
520524

@@ -555,6 +559,12 @@ private <T> Mono<T> insertCheckpoint(Mono<T> result, int statusCode, HttpRequest
555559
return result.checkpoint(description);
556560
}
557561

562+
private <T> Mono<T> wrapException(Throwable throwable, ClientResponse response) {
563+
return response.createException()
564+
.map(responseException -> responseException.initCause(throwable))
565+
.flatMap(Mono::error);
566+
}
567+
558568
@Override
559569
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass) {
560570
return this.responseMono.flatMap(response ->

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

+5
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
104104
.connect(httpMethod, url, httpRequest -> clientRequest.writeTo(httpRequest, this.strategies))
105105
.doOnRequest(n -> logRequest(clientRequest))
106106
.doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)"))
107+
.onErrorResume(WebClientUtils::shouldWrapException, t -> wrapException(t, clientRequest))
107108
.map(httpResponse -> {
108109
logResponse(httpResponse, logPrefix);
109110
return new DefaultClientResponse(
@@ -132,6 +133,10 @@ private String formatHeaders(HttpHeaders headers) {
132133
return this.enableLoggingRequestDetails ? headers.toString() : headers.isEmpty() ? "{}" : "{masked}";
133134
}
134135

136+
private <T> Mono<T> wrapException(Throwable t, ClientRequest r) {
137+
return Mono.error(() -> new WebClientRequestException(t, r.method(), r.url(), r.headers()));
138+
}
139+
135140
private HttpRequest createRequest(ClientRequest request) {
136141
return new HttpRequest() {
137142

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-2020 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.web.reactive.function.client;
18+
19+
import java.net.URI;
20+
21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.http.HttpMethod;
23+
24+
/**
25+
* Exceptions that contain actual HTTP request data.
26+
*
27+
* @author Arjen Poutsma
28+
* @since 5.3
29+
*/
30+
public class WebClientRequestException extends WebClientException {
31+
32+
private static final long serialVersionUID = -5139991985321385005L;
33+
34+
35+
private final HttpMethod method;
36+
37+
private final URI uri;
38+
39+
private final HttpHeaders headers;
40+
41+
42+
/**
43+
* Constructor for throwable.
44+
*/
45+
public WebClientRequestException(Throwable ex, HttpMethod method, URI uri, HttpHeaders headers) {
46+
super(ex.getMessage(), ex);
47+
48+
this.method = method;
49+
this.uri = uri;
50+
this.headers = headers;
51+
}
52+
53+
/**
54+
* Return the HTTP request method.
55+
*/
56+
public HttpMethod getMethod() {
57+
return this.method;
58+
}
59+
60+
/**
61+
* Return the request URI.
62+
*/
63+
public URI getUri() {
64+
return this.uri;
65+
}
66+
67+
/**
68+
* Return the HTTP request headers.
69+
*/
70+
public HttpHeaders getHeaders() {
71+
return this.headers;
72+
}
73+
74+
}

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

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import reactor.core.publisher.Flux;
2323
import reactor.core.publisher.Mono;
2424

25+
import org.springframework.core.codec.CodecException;
2526
import org.springframework.http.ResponseEntity;
2627

2728
/**
@@ -56,4 +57,10 @@ public static <T> Mono<ResponseEntity<List<T>>> mapToEntityList(ClientResponse r
5657
.body(list));
5758
}
5859

60+
/**
61+
* Indicates whether the given exception should be wrapped.
62+
*/
63+
public static boolean shouldWrapException(Throwable t) {
64+
return !(t instanceof WebClientException) && !(t instanceof CodecException);
65+
}
5966
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.http.ResponseEntity;
4141
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
4242
import org.springframework.http.client.reactive.ReactorResourceFactory;
43-
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
4443

4544
import static org.assertj.core.api.Assertions.assertThat;
4645
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
@@ -127,7 +126,7 @@ void bodyToMonoVoidWithoutContentType(String displayName, DataBufferFactory buff
127126
.retrieve()
128127
.bodyToMono(new ParameterizedTypeReference<Map<String, String>>() {});
129128

130-
StepVerifier.create(mono).expectError(UnsupportedMediaTypeException.class).verify(Duration.ofSeconds(3));
129+
StepVerifier.create(mono).expectError(WebClientResponseException.class).verify(Duration.ofSeconds(3));
131130
assertThat(this.server.getRequestCount()).isEqualTo(1);
132131
}
133132

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,12 @@ void exchangeWithRelativeUrl(ClientHttpConnector connector) {
10131013
Mono<ClientResponse> responseMono = WebClient.builder().build().get().uri(uri).exchange();
10141014

10151015
StepVerifier.create(responseMono)
1016-
.expectErrorMessage("URI is not absolute: " + uri)
1016+
.expectErrorSatisfies(throwable -> {
1017+
assertThat(throwable).isInstanceOf(WebClientRequestException.class);
1018+
WebClientRequestException ex = (WebClientRequestException) throwable;
1019+
assertThat(ex.getMethod()).isEqualTo(HttpMethod.GET);
1020+
assertThat(ex.getUri()).isEqualTo(URI.create(uri));
1021+
})
10171022
.verify(Duration.ofSeconds(5));
10181023
}
10191024

@@ -1126,6 +1131,25 @@ void exchangeResponseCookies(ClientHttpConnector connector) {
11261131
expectRequestCount(1);
11271132
}
11281133

1134+
@ParameterizedWebClientTest
1135+
void invalidDomain(ClientHttpConnector connector) {
1136+
startServer(connector);
1137+
1138+
String url = "http://example.invalid";
1139+
Mono<ClientResponse> result = this.webClient.get().
1140+
uri(url)
1141+
.exchange();
1142+
1143+
StepVerifier.create(result)
1144+
.expectErrorSatisfies(throwable -> {
1145+
assertThat(throwable).isInstanceOf(WebClientRequestException.class);
1146+
WebClientRequestException ex = (WebClientRequestException) throwable;
1147+
assertThat(ex.getMethod()).isEqualTo(HttpMethod.GET);
1148+
assertThat(ex.getUri()).isEqualTo(URI.create(url));
1149+
})
1150+
.verify();
1151+
}
1152+
11291153

11301154
private void prepareResponse(Consumer<MockResponse> consumer) {
11311155
MockResponse response = new MockResponse();

0 commit comments

Comments
 (0)