Skip to content

Commit b420782

Browse files
committed
Add ClientResponse::createException
This commit adds the createException() method to ClientResponse, returning a delayed WebClientResponseException based on the status code, headers, and body as well as the corresponding request. Closes gh-22825
1 parent 5e9a22d commit b420782

File tree

7 files changed

+128
-56
lines changed

7 files changed

+128
-56
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import org.springframework.core.ParameterizedTypeReference;
2929
import org.springframework.core.io.buffer.DataBuffer;
3030
import org.springframework.http.HttpHeaders;
31+
import org.springframework.http.HttpRequest;
3132
import org.springframework.http.HttpStatus;
3233
import org.springframework.http.MediaType;
3334
import org.springframework.http.ResponseCookie;
@@ -162,6 +163,14 @@ public interface ClientResponse {
162163
*/
163164
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
164165

166+
/**
167+
* Creates a {@link WebClientResponseException} based on the status code,
168+
* headers, and body of this response as well as the corresponding request.
169+
*
170+
* @return a {@code Mono} with a {@code WebClientResponseException} based on this response
171+
*/
172+
Mono<WebClientResponseException> createException();
173+
165174

166175
// Static builder methods
167176

@@ -317,6 +326,13 @@ interface Builder {
317326
*/
318327
Builder body(String body);
319328

329+
/**
330+
* Set the request associated with the response.
331+
* @param request the request
332+
* @return this builder
333+
*/
334+
Builder request(HttpRequest request);
335+
320336
/**
321337
* Build the response.
322338
*/

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,31 @@
1616

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

19+
import java.nio.charset.Charset;
20+
import java.nio.charset.StandardCharsets;
1921
import java.util.Collections;
2022
import java.util.List;
2123
import java.util.Map;
2224
import java.util.Optional;
2325
import java.util.OptionalLong;
26+
import java.util.function.Supplier;
2427

2528
import reactor.core.publisher.Flux;
2629
import reactor.core.publisher.Mono;
2730

2831
import org.springframework.core.ParameterizedTypeReference;
2932
import org.springframework.core.codec.Hints;
33+
import org.springframework.core.io.buffer.DataBufferUtils;
3034
import org.springframework.http.HttpHeaders;
35+
import org.springframework.http.HttpRequest;
3136
import org.springframework.http.HttpStatus;
3237
import org.springframework.http.MediaType;
3338
import org.springframework.http.ResponseCookie;
3439
import org.springframework.http.ResponseEntity;
3540
import org.springframework.http.client.reactive.ClientHttpResponse;
3641
import org.springframework.http.codec.HttpMessageReader;
3742
import org.springframework.http.server.reactive.ServerHttpResponse;
43+
import org.springframework.util.MimeType;
3844
import org.springframework.util.MultiValueMap;
3945
import org.springframework.web.reactive.function.BodyExtractor;
4046
import org.springframework.web.reactive.function.BodyExtractors;
@@ -58,15 +64,18 @@ class DefaultClientResponse implements ClientResponse {
5864

5965
private final String requestDescription;
6066

67+
private final Supplier<HttpRequest> requestSupplier;
68+
6169

6270
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies,
63-
String logPrefix, String requestDescription) {
71+
String logPrefix, String requestDescription, Supplier<HttpRequest> requestSupplier) {
6472

6573
this.response = response;
6674
this.strategies = strategies;
6775
this.headers = new DefaultHeaders();
6876
this.logPrefix = logPrefix;
6977
this.requestDescription = requestDescription;
78+
this.requestSupplier = requestSupplier;
7079
}
7180

7281

@@ -175,6 +184,46 @@ public <T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference
175184
return toEntityListInternal(bodyToFlux(elementTypeRef));
176185
}
177186

187+
@Override
188+
public Mono<WebClientResponseException> createException() {
189+
return DataBufferUtils.join(body(BodyExtractors.toDataBuffers()))
190+
.map(dataBuffer -> {
191+
byte[] bytes = new byte[dataBuffer.readableByteCount()];
192+
dataBuffer.read(bytes);
193+
DataBufferUtils.release(dataBuffer);
194+
return bytes;
195+
})
196+
.defaultIfEmpty(new byte[0])
197+
.map(bodyBytes -> {
198+
HttpRequest request = this.requestSupplier.get();
199+
Charset charset = headers().contentType()
200+
.map(MimeType::getCharset)
201+
.orElse(StandardCharsets.ISO_8859_1);
202+
if (HttpStatus.resolve(rawStatusCode()) != null) {
203+
return WebClientResponseException.create(
204+
statusCode().value(),
205+
statusCode().getReasonPhrase(),
206+
headers().asHttpHeaders(),
207+
bodyBytes,
208+
charset,
209+
request);
210+
}
211+
else {
212+
return new UnknownHttpStatusCodeException(
213+
rawStatusCode(),
214+
headers().asHttpHeaders(),
215+
bodyBytes,
216+
charset,
217+
request);
218+
}
219+
});
220+
}
221+
222+
// Used by DefaultClientResponseBuilder
223+
HttpRequest request() {
224+
return this.requestSupplier.get();
225+
}
226+
178227
private <T> Mono<ResponseEntity<List<T>>> toEntityListInternal(Flux<T> bodyFlux) {
179228
HttpHeaders headers = headers().asHttpHeaders();
180229
int status = rawStatusCode();

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import org.springframework.core.io.buffer.DataBufferUtils;
2727
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
2828
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.HttpRequest;
2930
import org.springframework.http.HttpStatus;
3031
import org.springframework.http.ResponseCookie;
3132
import org.springframework.http.client.reactive.ClientHttpResponse;
33+
import org.springframework.lang.Nullable;
3234
import org.springframework.util.Assert;
3335
import org.springframework.util.CollectionUtils;
3436
import org.springframework.util.LinkedMultiValueMap;
@@ -52,6 +54,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
5254

5355
private Flux<DataBuffer> body = Flux.empty();
5456

57+
@Nullable
58+
private HttpRequest request;
59+
5560

5661
public DefaultClientResponseBuilder(ExchangeStrategies strategies) {
5762
Assert.notNull(strategies, "ExchangeStrategies must not be null");
@@ -64,6 +69,9 @@ public DefaultClientResponseBuilder(ClientResponse other) {
6469
statusCode(other.statusCode());
6570
headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
6671
cookies(cookies -> cookies.addAll(other.cookies()));
72+
if (other instanceof DefaultClientResponse) {
73+
this.request = ((DefaultClientResponse) other).request();
74+
}
6775
}
6876

6977

@@ -127,6 +135,13 @@ private void releaseBody() {
127135
this.body.subscribe(DataBufferUtils.releaseConsumer());
128136
}
129137

138+
@Override
139+
public ClientResponse.Builder request(HttpRequest request) {
140+
Assert.notNull(request, "Request must not be null");
141+
this.request = request;
142+
return this;
143+
}
144+
130145
@Override
131146
public ClientResponse build() {
132147

@@ -136,7 +151,7 @@ public ClientResponse build() {
136151
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
137152
// e.g. via ClientResponse.Builder, but this (builder) is not used currently.
138153

139-
return new DefaultClientResponse(httpResponse, this.strategies, "", "");
154+
return new DefaultClientResponse(httpResponse, this.strategies, "", "", () -> this.request);
140155
}
141156

142157

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

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@
1818

1919
import java.net.URI;
2020
import java.nio.charset.Charset;
21-
import java.nio.charset.StandardCharsets;
2221
import java.time.ZonedDateTime;
2322
import java.util.ArrayList;
2423
import java.util.Arrays;
2524
import java.util.LinkedHashMap;
2625
import java.util.List;
2726
import java.util.Map;
28-
import java.util.function.BiFunction;
2927
import java.util.function.Consumer;
3028
import java.util.function.Function;
3129
import java.util.function.Predicate;
@@ -36,7 +34,6 @@
3634
import reactor.core.publisher.Mono;
3735

3836
import org.springframework.core.ParameterizedTypeReference;
39-
import org.springframework.core.io.buffer.DataBufferUtils;
4037
import org.springframework.http.HttpHeaders;
4138
import org.springframework.http.HttpMethod;
4239
import org.springframework.http.HttpRequest;
@@ -47,9 +44,7 @@
4744
import org.springframework.util.Assert;
4845
import org.springframework.util.CollectionUtils;
4946
import org.springframework.util.LinkedMultiValueMap;
50-
import org.springframework.util.MimeType;
5147
import org.springframework.util.MultiValueMap;
52-
import org.springframework.web.reactive.function.BodyExtractors;
5348
import org.springframework.web.reactive.function.BodyInserter;
5449
import org.springframework.web.reactive.function.BodyInserters;
5550
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -421,7 +416,7 @@ public HttpHeaders getHeaders() {
421416
private static class DefaultResponseSpec implements ResponseSpec {
422417

423418
private static final StatusHandler DEFAULT_STATUS_HANDLER =
424-
new StatusHandler(HttpStatus::isError, DefaultResponseSpec::createResponseException);
419+
new StatusHandler(HttpStatus::isError, ClientResponse::createException);
425420

426421
private final Mono<ClientResponse> responseMono;
427422

@@ -442,8 +437,7 @@ public ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
442437
if (this.statusHandlers.size() == 1 && this.statusHandlers.get(0) == DEFAULT_STATUS_HANDLER) {
443438
this.statusHandlers.clear();
444439
}
445-
this.statusHandlers.add(new StatusHandler(statusPredicate,
446-
(clientResponse, request) -> exceptionFunction.apply(clientResponse)));
440+
this.statusHandlers.add(new StatusHandler(statusPredicate, exceptionFunction));
447441

448442
return this;
449443
}
@@ -478,24 +472,24 @@ private <T extends Publisher<?>> T handleBody(ClientResponse response,
478472
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
479473
for (StatusHandler handler : this.statusHandlers) {
480474
if (handler.test(response.statusCode())) {
481-
HttpRequest request = this.requestSupplier.get();
482475
Mono<? extends Throwable> exMono;
483476
try {
484-
exMono = handler.apply(response, request);
477+
exMono = handler.apply(response);
485478
exMono = exMono.flatMap(ex -> drainBody(response, ex));
486479
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
487480
}
488481
catch (Throwable ex2) {
489482
exMono = drainBody(response, ex2);
490483
}
491484
T result = errorFunction.apply(exMono);
485+
HttpRequest request = this.requestSupplier.get();
492486
return insertCheckpoint(result, response.statusCode(), request);
493487
}
494488
}
495489
return bodyPublisher;
496490
}
497491
else {
498-
return errorFunction.apply(createResponseException(response, this.requestSupplier.get()));
492+
return errorFunction.apply(response.createException());
499493
}
500494
}
501495

@@ -523,50 +517,15 @@ else if (result instanceof Flux) {
523517
}
524518
}
525519

526-
private static Mono<WebClientResponseException> createResponseException(
527-
ClientResponse response, HttpRequest request) {
528-
529-
return DataBufferUtils.join(response.body(BodyExtractors.toDataBuffers()))
530-
.map(dataBuffer -> {
531-
byte[] bytes = new byte[dataBuffer.readableByteCount()];
532-
dataBuffer.read(bytes);
533-
DataBufferUtils.release(dataBuffer);
534-
return bytes;
535-
})
536-
.defaultIfEmpty(new byte[0])
537-
.map(bodyBytes -> {
538-
Charset charset = response.headers().contentType()
539-
.map(MimeType::getCharset)
540-
.orElse(StandardCharsets.ISO_8859_1);
541-
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
542-
return WebClientResponseException.create(
543-
response.statusCode().value(),
544-
response.statusCode().getReasonPhrase(),
545-
response.headers().asHttpHeaders(),
546-
bodyBytes,
547-
charset,
548-
request);
549-
}
550-
else {
551-
return new UnknownHttpStatusCodeException(
552-
response.rawStatusCode(),
553-
response.headers().asHttpHeaders(),
554-
bodyBytes,
555-
charset,
556-
request);
557-
}
558-
});
559-
}
560-
561520

562521
private static class StatusHandler {
563522

564523
private final Predicate<HttpStatus> predicate;
565524

566-
private final BiFunction<ClientResponse, HttpRequest, Mono<? extends Throwable>> exceptionFunction;
525+
private final Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction;
567526

568527
public StatusHandler(Predicate<HttpStatus> predicate,
569-
BiFunction<ClientResponse, HttpRequest, Mono<? extends Throwable>> exceptionFunction) {
528+
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
570529

571530
Assert.notNull(predicate, "Predicate must not be null");
572531
Assert.notNull(exceptionFunction, "Function must not be null");
@@ -578,8 +537,8 @@ public boolean test(HttpStatus status) {
578537
return this.predicate.test(status);
579538
}
580539

581-
public Mono<? extends Throwable> apply(ClientResponse response, HttpRequest request) {
582-
return this.exceptionFunction.apply(response, request);
540+
public Mono<? extends Throwable> apply(ClientResponse response) {
541+
return this.exceptionFunction.apply(response);
583542
}
584543
}
585544
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.core.log.LogFormatUtils;
2626
import org.springframework.http.HttpHeaders;
2727
import org.springframework.http.HttpMethod;
28+
import org.springframework.http.HttpRequest;
2829
import org.springframework.http.HttpStatus;
2930
import org.springframework.http.client.reactive.ClientHttpConnector;
3031
import org.springframework.http.client.reactive.ClientHttpResponse;
@@ -106,7 +107,8 @@ public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
106107
.map(httpResponse -> {
107108
logResponse(httpResponse, logPrefix);
108109
return new DefaultClientResponse(
109-
httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url);
110+
httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url,
111+
() -> createRequest(clientRequest));
110112
});
111113
}
112114

@@ -129,6 +131,31 @@ private void logResponse(ClientHttpResponse response, String logPrefix) {
129131
private String formatHeaders(HttpHeaders headers) {
130132
return this.enableLoggingRequestDetails ? headers.toString() : headers.isEmpty() ? "{}" : "{masked}";
131133
}
134+
135+
private HttpRequest createRequest(ClientRequest request) {
136+
return new HttpRequest() {
137+
138+
@Override
139+
public HttpMethod getMethod() {
140+
return request.method();
141+
}
142+
143+
@Override
144+
public String getMethodValue() {
145+
return request.method().name();
146+
}
147+
148+
@Override
149+
public URI getURI() {
150+
return request.url();
151+
}
152+
153+
@Override
154+
public HttpHeaders getHeaders() {
155+
return request.headers();
156+
}
157+
};
158+
}
132159
}
133160

134161
}

0 commit comments

Comments
 (0)