diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java index a9fd5cedd0940f60d973a8284a7ee9ec4241f56a..02bbcd139f07048899a0b6f3c65fdd5a5925e62f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java @@ -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"); * 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 { */ Mono>> toEntityList(ParameterizedTypeReference 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 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. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index ae9787e60bdde890eeec18ed12594c7520a5c255..762d7e1a22d477968d683816e4fdbca4a83dd915 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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 requestSupplier; + public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies, - String logPrefix, String requestDescription) { + String logPrefix, String requestDescription, Supplier 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 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 Mono>> toEntityListInternal(Flux bodyFlux) { HttpHeaders headers = headers().asHttpHeaders(); int status = rawStatusCode(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index ff0374442b933de94502d4e6c37e734592fedc62..d5c540b097dab9a84c1964fef13d4c49a8ccff50 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -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 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); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 3e309f4e4405c4b639d83ae7ab59e28071e51179..f40febfe1ffe5de3e55867ac65f555d66a8fe33a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -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 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 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 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 predicate; - private final BiFunction> exceptionFunction; + private final Function> exceptionFunction; public StatusHandler(Predicate predicate, - BiFunction> exceptionFunction) { + Function> 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 apply(ClientResponse response, HttpRequest request) { - return this.exceptionFunction.apply(response, request); + public Mono apply(ClientResponse response) { + return this.exceptionFunction.apply(response); } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java index a505136e2488a39ca2fe0ce35b19c00140b3adcb..b13651cc844bbe920544d1318675c1757770d8a9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java @@ -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(); + } + }; + } } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java index 49291ed08edcbf5bc4e5a02b85ce325dbf1337dd..d1d3c6a59c583f3cd2dc4d114945dc5a1b17a26a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/ClientResponseWrapper.java @@ -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"); * 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 createException() { + return this.delegate.createException(); + } + /** * Implementation of the {@code Headers} interface that can be subclassed * to adapt the headers in a diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index 86f9c4cc982a1873cea266d2135f8efdf1672218..5bc0fafb3ba6dc247a27680bdb8b57dac6474fab 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -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); }