提交 b4207823 编写于 作者: A Arjen Poutsma

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
上级 5e9a22d1
/*
* 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");
* 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.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
......@@ -162,6 +163,14 @@ public interface ClientResponse {
*/
<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
......@@ -317,6 +326,13 @@ public interface ClientResponse {
*/
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.
*/
......
......@@ -16,18 +16,23 @@
package org.springframework.web.reactive.function.client;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.reactive.function.BodyExtractors;
......@@ -58,15 +64,18 @@ class DefaultClientResponse implements ClientResponse {
private final String requestDescription;
private final Supplier<HttpRequest> requestSupplier;
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies,
String logPrefix, String requestDescription) {
String logPrefix, String requestDescription, Supplier<HttpRequest> requestSupplier) {
this.response = response;
this.strategies = strategies;
this.headers = new DefaultHeaders();
this.logPrefix = logPrefix;
this.requestDescription = requestDescription;
this.requestSupplier = requestSupplier;
}
......@@ -175,6 +184,46 @@ class DefaultClientResponse implements ClientResponse {
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) {
HttpHeaders headers = headers().asHttpHeaders();
int status = rawStatusCode();
......
......@@ -26,9 +26,11 @@ import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
......@@ -52,6 +54,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
private Flux<DataBuffer> body = Flux.empty();
@Nullable
private HttpRequest request;
public DefaultClientResponseBuilder(ExchangeStrategies strategies) {
Assert.notNull(strategies, "ExchangeStrategies must not be null");
......@@ -64,6 +69,9 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
statusCode(other.statusCode());
headers(headers -> headers.addAll(other.headers().asHttpHeaders()));
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());
}
@Override
public ClientResponse.Builder request(HttpRequest request) {
Assert.notNull(request, "Request must not be null");
this.request = request;
return this;
}
@Override
public ClientResponse build() {
......@@ -136,7 +151,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
// 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);
}
......
......@@ -18,14 +18,12 @@ package org.springframework.web.reactive.function.client;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
......@@ -36,7 +34,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
......@@ -47,9 +44,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeType;
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.BodyInserters;
import org.springframework.web.util.DefaultUriBuilderFactory;
......@@ -421,7 +416,7 @@ class DefaultWebClient implements WebClient {
private static class DefaultResponseSpec implements ResponseSpec {
private static final StatusHandler DEFAULT_STATUS_HANDLER =
new StatusHandler(HttpStatus::isError, DefaultResponseSpec::createResponseException);
new StatusHandler(HttpStatus::isError, ClientResponse::createException);
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) {
this.statusHandlers.clear();
}
this.statusHandlers.add(new StatusHandler(statusPredicate,
(clientResponse, request) -> exceptionFunction.apply(clientResponse)));
this.statusHandlers.add(new StatusHandler(statusPredicate, exceptionFunction));
return this;
}
......@@ -478,10 +472,9 @@ class DefaultWebClient implements WebClient {
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
for (StatusHandler handler : this.statusHandlers) {
if (handler.test(response.statusCode())) {
HttpRequest request = this.requestSupplier.get();
Mono<? extends Throwable> exMono;
try {
exMono = handler.apply(response, request);
exMono = handler.apply(response);
exMono = exMono.flatMap(ex -> drainBody(response, ex));
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
}
......@@ -489,13 +482,14 @@ class DefaultWebClient implements WebClient {
exMono = drainBody(response, ex2);
}
T result = errorFunction.apply(exMono);
HttpRequest request = this.requestSupplier.get();
return insertCheckpoint(result, response.statusCode(), request);
}
}
return bodyPublisher;
}
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 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,
BiFunction<ClientResponse, HttpRequest, Mono<? extends Throwable>> exceptionFunction) {
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
Assert.notNull(predicate, "Predicate 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);
}
public Mono<? extends Throwable> apply(ClientResponse response, HttpRequest request) {
return this.exceptionFunction.apply(response, request);
public Mono<? extends Throwable> apply(ClientResponse response) {
return this.exceptionFunction.apply(response);
}
}
}
......
......@@ -25,6 +25,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpResponse;
......@@ -106,7 +107,8 @@ public abstract class ExchangeFunctions {
.map(httpResponse -> {
logResponse(httpResponse, logPrefix);
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) {
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();
}
};
}
}
}
/*
* 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");
* 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.client.ClientResponse;
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
......@@ -137,6 +138,11 @@ public class ClientResponseWrapper implements ClientResponse {
return this.delegate.toEntityList(elementTypeRef);
}
@Override
public Mono<WebClientResponseException> createException() {
return this.delegate.createException();
}
/**
* Implementation of the {@code Headers} interface that can be subclassed
* to adapt the headers in a
......
......@@ -69,7 +69,7 @@ public class DefaultClientResponseTests {
public void createMocks() {
mockResponse = mock(ClientHttpResponse.class);
mockExchangeStrategies = mock(ExchangeStrategies.class);
defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies, "", "");
defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies, "", "", () -> null);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册