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
This commit is contained in:
Arjen Poutsma 2019-07-11 15:42:57 +02:00
parent 5e9a22d118
commit b4207823af
7 changed files with 128 additions and 56 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
@ -162,6 +163,14 @@ public interface ClientResponse {
*/ */
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef); <T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
/**
* Creates a {@link WebClientResponseException} based on the status code,
* headers, and body of this response as well as the corresponding request.
*
* @return a {@code Mono} with a {@code WebClientResponseException} based on this response
*/
Mono<WebClientResponseException> createException();
// Static builder methods // Static builder methods
@ -317,6 +326,13 @@ public interface ClientResponse {
*/ */
Builder body(String body); Builder body(String body);
/**
* Set the request associated with the response.
* @param request the request
* @return this builder
*/
Builder request(HttpRequest request);
/** /**
* Build the response. * Build the response.
*/ */

View File

@ -16,18 +16,23 @@
package org.springframework.web.reactive.function.client; package org.springframework.web.reactive.function.client;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.util.function.Supplier;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.Hints; import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
@ -35,6 +40,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.BodyExtractors;
@ -58,15 +64,18 @@ class DefaultClientResponse implements ClientResponse {
private final String requestDescription; private final String requestDescription;
private final Supplier<HttpRequest> requestSupplier;
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies, public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies,
String logPrefix, String requestDescription) { String logPrefix, String requestDescription, Supplier<HttpRequest> requestSupplier) {
this.response = response; this.response = response;
this.strategies = strategies; this.strategies = strategies;
this.headers = new DefaultHeaders(); this.headers = new DefaultHeaders();
this.logPrefix = logPrefix; this.logPrefix = logPrefix;
this.requestDescription = requestDescription; this.requestDescription = requestDescription;
this.requestSupplier = requestSupplier;
} }
@ -175,6 +184,46 @@ class DefaultClientResponse implements ClientResponse {
return toEntityListInternal(bodyToFlux(elementTypeRef)); return toEntityListInternal(bodyToFlux(elementTypeRef));
} }
@Override
public Mono<WebClientResponseException> createException() {
return DataBufferUtils.join(body(BodyExtractors.toDataBuffers()))
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.defaultIfEmpty(new byte[0])
.map(bodyBytes -> {
HttpRequest request = this.requestSupplier.get();
Charset charset = headers().contentType()
.map(MimeType::getCharset)
.orElse(StandardCharsets.ISO_8859_1);
if (HttpStatus.resolve(rawStatusCode()) != null) {
return WebClientResponseException.create(
statusCode().value(),
statusCode().getReasonPhrase(),
headers().asHttpHeaders(),
bodyBytes,
charset,
request);
}
else {
return new UnknownHttpStatusCodeException(
rawStatusCode(),
headers().asHttpHeaders(),
bodyBytes,
charset,
request);
}
});
}
// Used by DefaultClientResponseBuilder
HttpRequest request() {
return this.requestSupplier.get();
}
private <T> Mono<ResponseEntity<List<T>>> toEntityListInternal(Flux<T> bodyFlux) { private <T> Mono<ResponseEntity<List<T>>> toEntityListInternal(Flux<T> bodyFlux) {
HttpHeaders headers = headers().asHttpHeaders(); HttpHeaders headers = headers().asHttpHeaders();
int status = rawStatusCode(); int status = rawStatusCode();

View File

@ -26,9 +26,11 @@ import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
@ -52,6 +54,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
private Flux<DataBuffer> body = Flux.empty(); private Flux<DataBuffer> body = Flux.empty();
@Nullable
private HttpRequest request;
public DefaultClientResponseBuilder(ExchangeStrategies strategies) { public DefaultClientResponseBuilder(ExchangeStrategies strategies) {
Assert.notNull(strategies, "ExchangeStrategies must not be null"); Assert.notNull(strategies, "ExchangeStrategies must not be null");
@ -64,6 +69,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
statusCode(other.statusCode()); statusCode(other.statusCode());
headers(headers -> headers.addAll(other.headers().asHttpHeaders())); headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
cookies(cookies -> cookies.addAll(other.cookies())); cookies(cookies -> cookies.addAll(other.cookies()));
if (other instanceof DefaultClientResponse) {
this.request = ((DefaultClientResponse) other).request();
}
} }
@ -127,6 +135,13 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
this.body.subscribe(DataBufferUtils.releaseConsumer()); this.body.subscribe(DataBufferUtils.releaseConsumer());
} }
@Override
public ClientResponse.Builder request(HttpRequest request) {
Assert.notNull(request, "Request must not be null");
this.request = request;
return this;
}
@Override @Override
public ClientResponse build() { public ClientResponse build() {
@ -136,7 +151,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed, // When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
// e.g. via ClientResponse.Builder, but this (builder) is not used currently. // e.g. via ClientResponse.Builder, but this (builder) is not used currently.
return new DefaultClientResponse(httpResponse, this.strategies, "", ""); return new DefaultClientResponse(httpResponse, this.strategies, "", "", () -> this.request);
} }

View File

@ -18,14 +18,12 @@ package org.springframework.web.reactive.function.client;
import java.net.URI; import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -36,7 +34,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest; import org.springframework.http.HttpRequest;
@ -47,9 +44,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.DefaultUriBuilderFactory;
@ -421,7 +416,7 @@ class DefaultWebClient implements WebClient {
private static class DefaultResponseSpec implements ResponseSpec { private static class DefaultResponseSpec implements ResponseSpec {
private static final StatusHandler DEFAULT_STATUS_HANDLER = private static final StatusHandler DEFAULT_STATUS_HANDLER =
new StatusHandler(HttpStatus::isError, DefaultResponseSpec::createResponseException); new StatusHandler(HttpStatus::isError, ClientResponse::createException);
private final Mono<ClientResponse> responseMono; private final Mono<ClientResponse> responseMono;
@ -442,8 +437,7 @@ class DefaultWebClient implements WebClient {
if (this.statusHandlers.size() == 1 && this.statusHandlers.get(0) == DEFAULT_STATUS_HANDLER) { if (this.statusHandlers.size() == 1 && this.statusHandlers.get(0) == DEFAULT_STATUS_HANDLER) {
this.statusHandlers.clear(); this.statusHandlers.clear();
} }
this.statusHandlers.add(new StatusHandler(statusPredicate, this.statusHandlers.add(new StatusHandler(statusPredicate, exceptionFunction));
(clientResponse, request) -> exceptionFunction.apply(clientResponse)));
return this; return this;
} }
@ -478,10 +472,9 @@ class DefaultWebClient implements WebClient {
if (HttpStatus.resolve(response.rawStatusCode()) != null) { if (HttpStatus.resolve(response.rawStatusCode()) != null) {
for (StatusHandler handler : this.statusHandlers) { for (StatusHandler handler : this.statusHandlers) {
if (handler.test(response.statusCode())) { if (handler.test(response.statusCode())) {
HttpRequest request = this.requestSupplier.get();
Mono<? extends Throwable> exMono; Mono<? extends Throwable> exMono;
try { try {
exMono = handler.apply(response, request); exMono = handler.apply(response);
exMono = exMono.flatMap(ex -> drainBody(response, ex)); exMono = exMono.flatMap(ex -> drainBody(response, ex));
exMono = exMono.onErrorResume(ex -> drainBody(response, ex)); exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
} }
@ -489,13 +482,14 @@ class DefaultWebClient implements WebClient {
exMono = drainBody(response, ex2); exMono = drainBody(response, ex2);
} }
T result = errorFunction.apply(exMono); T result = errorFunction.apply(exMono);
HttpRequest request = this.requestSupplier.get();
return insertCheckpoint(result, response.statusCode(), request); return insertCheckpoint(result, response.statusCode(), request);
} }
} }
return bodyPublisher; return bodyPublisher;
} }
else { else {
return errorFunction.apply(createResponseException(response, this.requestSupplier.get())); return errorFunction.apply(response.createException());
} }
} }
@ -523,50 +517,15 @@ class DefaultWebClient implements WebClient {
} }
} }
private static Mono<WebClientResponseException> createResponseException(
ClientResponse response, HttpRequest request) {
return DataBufferUtils.join(response.body(BodyExtractors.toDataBuffers()))
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.defaultIfEmpty(new byte[0])
.map(bodyBytes -> {
Charset charset = response.headers().contentType()
.map(MimeType::getCharset)
.orElse(StandardCharsets.ISO_8859_1);
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
return WebClientResponseException.create(
response.statusCode().value(),
response.statusCode().getReasonPhrase(),
response.headers().asHttpHeaders(),
bodyBytes,
charset,
request);
}
else {
return new UnknownHttpStatusCodeException(
response.rawStatusCode(),
response.headers().asHttpHeaders(),
bodyBytes,
charset,
request);
}
});
}
private static class StatusHandler { private static class StatusHandler {
private final Predicate<HttpStatus> predicate; private final Predicate<HttpStatus> predicate;
private final BiFunction<ClientResponse, HttpRequest, Mono<? extends Throwable>> exceptionFunction; private final Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction;
public StatusHandler(Predicate<HttpStatus> predicate, public StatusHandler(Predicate<HttpStatus> predicate,
BiFunction<ClientResponse, HttpRequest, Mono<? extends Throwable>> exceptionFunction) { Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
Assert.notNull(predicate, "Predicate must not be null"); Assert.notNull(predicate, "Predicate must not be null");
Assert.notNull(exceptionFunction, "Function must not be null"); Assert.notNull(exceptionFunction, "Function must not be null");
@ -578,8 +537,8 @@ class DefaultWebClient implements WebClient {
return this.predicate.test(status); return this.predicate.test(status);
} }
public Mono<? extends Throwable> apply(ClientResponse response, HttpRequest request) { public Mono<? extends Throwable> apply(ClientResponse response) {
return this.exceptionFunction.apply(response, request); return this.exceptionFunction.apply(response);
} }
} }
} }

View File

@ -25,6 +25,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.log.LogFormatUtils; import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.client.reactive.ClientHttpResponse;
@ -106,7 +107,8 @@ public abstract class ExchangeFunctions {
.map(httpResponse -> { .map(httpResponse -> {
logResponse(httpResponse, logPrefix); logResponse(httpResponse, logPrefix);
return new DefaultClientResponse( return new DefaultClientResponse(
httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url); httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url,
() -> createRequest(clientRequest));
}); });
} }
@ -129,6 +131,31 @@ public abstract class ExchangeFunctions {
private String formatHeaders(HttpHeaders headers) { private String formatHeaders(HttpHeaders headers) {
return this.enableLoggingRequestDetails ? headers.toString() : headers.isEmpty() ? "{}" : "{masked}"; return this.enableLoggingRequestDetails ? headers.toString() : headers.isEmpty() ? "{}" : "{masked}";
} }
private HttpRequest createRequest(ClientRequest request) {
return new HttpRequest() {
@Override
public HttpMethod getMethod() {
return request.method();
}
@Override
public String getMethodValue() {
return request.method().name();
}
@Override
public URI getURI() {
return request.url();
}
@Override
public HttpHeaders getHeaders() {
return request.headers();
}
};
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,6 +35,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClientResponseException;
/** /**
* Implementation of the {@link ClientResponse} interface that can be subclassed * Implementation of the {@link ClientResponse} interface that can be subclassed
@ -137,6 +138,11 @@ public class ClientResponseWrapper implements ClientResponse {
return this.delegate.toEntityList(elementTypeRef); return this.delegate.toEntityList(elementTypeRef);
} }
@Override
public Mono<WebClientResponseException> createException() {
return this.delegate.createException();
}
/** /**
* Implementation of the {@code Headers} interface that can be subclassed * Implementation of the {@code Headers} interface that can be subclassed
* to adapt the headers in a * to adapt the headers in a

View File

@ -69,7 +69,7 @@ public class DefaultClientResponseTests {
public void createMocks() { public void createMocks() {
mockResponse = mock(ClientHttpResponse.class); mockResponse = mock(ClientHttpResponse.class);
mockExchangeStrategies = mock(ExchangeStrategies.class); mockExchangeStrategies = mock(ExchangeStrategies.class);
defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies, "", ""); defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies, "", "", () -> null);
} }