parent
64795664b2
commit
922636e85e
|
@ -74,6 +74,12 @@ public class ProblemDetail {
|
|||
this.instance = other.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* For deserialization.
|
||||
*/
|
||||
protected ProblemDetail() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Variant of {@link #setType(URI)} for chained initialization.
|
||||
|
|
|
@ -16,17 +16,26 @@
|
|||
|
||||
package org.springframework.web.client;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
|
@ -50,6 +59,20 @@ import org.springframework.util.ObjectUtils;
|
|||
*/
|
||||
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
|
||||
|
||||
@Nullable
|
||||
private List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
|
||||
/**
|
||||
* For internal use from the RestTemplate, to pass the message converters
|
||||
* to use to decode error content.
|
||||
* @since 6.0
|
||||
*/
|
||||
void setMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
this.messageConverters = Collections.unmodifiableList(converters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delegates to {@link #hasError(HttpStatusCode)} with the response status code.
|
||||
* @see ClientHttpResponse#getStatusCode()
|
||||
|
@ -155,15 +178,48 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
|
|||
Charset charset = getCharset(response);
|
||||
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
|
||||
|
||||
RestClientResponseException ex;
|
||||
if (statusCode.is4xxClientError()) {
|
||||
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
ex = HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
}
|
||||
else if (statusCode.is5xxServerError()) {
|
||||
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
ex = HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
|
||||
}
|
||||
else {
|
||||
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
|
||||
ex = new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(this.messageConverters)) {
|
||||
ex.setBodyConvertFunction(initBodyConvertFunction(response, body));
|
||||
}
|
||||
|
||||
throw ex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a function for decoding the error content. This can be passed to
|
||||
* {@link RestClientResponseException#setBodyConvertFunction(Function)}.
|
||||
* @since 6.0
|
||||
*/
|
||||
protected Function<ResolvableType, ?> initBodyConvertFunction(ClientHttpResponse response, byte[] body) {
|
||||
Assert.state(!CollectionUtils.isEmpty(this.messageConverters), "Expected message converters");
|
||||
return resolvableType -> {
|
||||
try {
|
||||
HttpMessageConverterExtractor<?> extractor =
|
||||
new HttpMessageConverterExtractor<>(resolvableType.getType(), this.messageConverters);
|
||||
|
||||
return extractor.extractData(new ClientHttpResponseDecorator(response) {
|
||||
@Override
|
||||
public InputStream getBody() {
|
||||
return new ByteArrayInputStream(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RestClientException(
|
||||
"Error while extracting response for type [" + resolvableType + "]", ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,10 +19,14 @@ package org.springframework.web.client;
|
|||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Common base class for exceptions that contain actual HTTP response data.
|
||||
|
@ -49,6 +53,9 @@ public class RestClientResponseException extends RestClientException {
|
|||
@Nullable
|
||||
private final String responseCharset;
|
||||
|
||||
@Nullable
|
||||
private Function<ResolvableType, ?> bodyConvertFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new instance of with the given response data.
|
||||
|
@ -153,4 +160,43 @@ public class RestClientResponseException extends RestClientException {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the error response content to the specified type.
|
||||
* @param targetType the type to convert to
|
||||
* @param <E> the expected target type
|
||||
* @return the converted object, or {@code null} if there is no content
|
||||
* @since 6.0
|
||||
*/
|
||||
@Nullable
|
||||
public <E> E getResponseBodyAs(Class<E> targetType) {
|
||||
return getResponseBodyAs(ResolvableType.forClass(targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #getResponseBodyAs(Class)} with
|
||||
* {@link ParameterizedTypeReference}.
|
||||
* @since 6.0
|
||||
*/
|
||||
@Nullable
|
||||
public <E> E getResponseBodyAs(ParameterizedTypeReference<E> targetType) {
|
||||
return getResponseBodyAs(ResolvableType.forType(targetType.getType()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private <E> E getResponseBodyAs(ResolvableType targetType) {
|
||||
Assert.state(this.bodyConvertFunction != null, "Function to convert body not set");
|
||||
return (E) this.bodyConvertFunction.apply(targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a function to use to decode the response error content
|
||||
* via {@link #getResponseBodyAs(Class)}.
|
||||
* @param bodyConvertFunction the function to use
|
||||
* @since 6.0
|
||||
*/
|
||||
public void setBodyConvertFunction(Function<ResolvableType, ?> bodyConvertFunction) {
|
||||
this.bodyConvertFunction = bodyConvertFunction;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
|
@ -195,6 +195,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
|||
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
|
||||
}
|
||||
|
||||
updateErrorHandlerConverters();
|
||||
this.uriTemplateHandler = initUriTemplateHandler();
|
||||
}
|
||||
|
||||
|
@ -219,9 +220,16 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
|||
validateConverters(messageConverters);
|
||||
this.messageConverters.addAll(messageConverters);
|
||||
this.uriTemplateHandler = initUriTemplateHandler();
|
||||
updateErrorHandlerConverters();
|
||||
}
|
||||
|
||||
|
||||
private void updateErrorHandlerConverters() {
|
||||
if (this.errorHandler instanceof DefaultResponseErrorHandler handler) {
|
||||
handler.setMessageConverters(this.messageConverters);
|
||||
}
|
||||
}
|
||||
|
||||
private static DefaultUriBuilderFactory initUriTemplateHandler() {
|
||||
DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory();
|
||||
uriFactory.setEncodingMode(EncodingMode.URI_COMPONENT); // for backwards compatibility..
|
||||
|
@ -240,6 +248,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
|||
this.messageConverters.clear();
|
||||
this.messageConverters.addAll(messageConverters);
|
||||
}
|
||||
updateErrorHandlerConverters();
|
||||
}
|
||||
|
||||
private void validateConverters(List<HttpMessageConverter<?>> messageConverters) {
|
||||
|
@ -262,6 +271,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
|||
public void setErrorHandler(ResponseErrorHandler errorHandler) {
|
||||
Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
|
||||
this.errorHandler = errorHandler;
|
||||
updateErrorHandlerConverters();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
|
@ -22,15 +22,19 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
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;
|
||||
|
@ -39,8 +43,11 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
|
@ -201,11 +208,15 @@ class DefaultClientResponse implements ClientResponse {
|
|||
.defaultIfEmpty(EMPTY)
|
||||
.onErrorReturn(ex -> !(ex instanceof Error), EMPTY)
|
||||
.map(bodyBytes -> {
|
||||
|
||||
HttpRequest request = this.requestSupplier.get();
|
||||
Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null);
|
||||
Optional<MediaType> mediaType = headers().contentType();
|
||||
Charset charset = mediaType.map(MimeType::getCharset).orElse(null);
|
||||
HttpStatusCode statusCode = statusCode();
|
||||
|
||||
WebClientResponseException exception;
|
||||
if (statusCode instanceof HttpStatus httpStatus) {
|
||||
return WebClientResponseException.create(
|
||||
exception = WebClientResponseException.create(
|
||||
statusCode,
|
||||
httpStatus.getReasonPhrase(),
|
||||
headers().asHttpHeaders(),
|
||||
|
@ -214,16 +225,35 @@ class DefaultClientResponse implements ClientResponse {
|
|||
request);
|
||||
}
|
||||
else {
|
||||
return new UnknownHttpStatusCodeException(
|
||||
exception = new UnknownHttpStatusCodeException(
|
||||
statusCode,
|
||||
headers().asHttpHeaders(),
|
||||
bodyBytes,
|
||||
charset,
|
||||
request);
|
||||
}
|
||||
exception.setBodyDecodeFunction(initDecodeFunction(bodyBytes, mediaType.orElse(null)));
|
||||
return exception;
|
||||
});
|
||||
}
|
||||
|
||||
private Function<ResolvableType, ?> initDecodeFunction(byte[] body, @Nullable MediaType contentType) {
|
||||
return targetType -> {
|
||||
Decoder<?> decoder = null;
|
||||
for (HttpMessageReader<?> reader : strategies().messageReaders()) {
|
||||
if (reader.canRead(targetType, contentType)) {
|
||||
if (reader instanceof DecoderHttpMessageReader<?> decoderReader) {
|
||||
decoder = decoderReader.getDecoder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.state(decoder != null, "No suitable decoder");
|
||||
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(body);
|
||||
return decoder.decode(buffer, targetType, null, Collections.emptyMap());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> createError() {
|
||||
return createException().flatMap(Mono::error);
|
||||
|
|
|
@ -18,12 +18,16 @@ package org.springframework.web.reactive.function.client;
|
|||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Exceptions that contain actual HTTP response data.
|
||||
|
@ -51,6 +55,10 @@ public class WebClientResponseException extends WebClientException {
|
|||
@Nullable
|
||||
private final HttpRequest request;
|
||||
|
||||
@SuppressWarnings("MutableException")
|
||||
@Nullable
|
||||
private Function<ResolvableType, ?> bodyDecodeFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with response data only, and a default message.
|
||||
|
@ -194,6 +202,37 @@ public class WebClientResponseException extends WebClientException {
|
|||
(this.responseCharset != null ? this.responseCharset : defaultCharset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the error content to the specified type.
|
||||
* @param targetType the type to decode to
|
||||
* @param <E> the expected target type
|
||||
* @return the decoded content, or {@code null} if there is no content
|
||||
* @throws IllegalStateException if a Decoder cannot be found
|
||||
* @throws org.springframework.core.codec.DecodingException if decoding fails
|
||||
* @since 6.0
|
||||
*/
|
||||
@Nullable
|
||||
public <E> E getResponseBodyAs(Class<E> targetType) {
|
||||
return getResponseBodyAs(ResolvableType.forClass(targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #getResponseBodyAs(Class)} with
|
||||
* {@link ParameterizedTypeReference}.
|
||||
* @since 6.0
|
||||
*/
|
||||
@Nullable
|
||||
public <E> E getResponseBodyAs(ParameterizedTypeReference<E> targetType) {
|
||||
return getResponseBodyAs(ResolvableType.forType(targetType.getType()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private <E> E getResponseBodyAs(ResolvableType targetType) {
|
||||
Assert.state(this.bodyDecodeFunction != null, "Decoder function not set");
|
||||
return (E) this.bodyDecodeFunction.apply(targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the corresponding request.
|
||||
* @since 5.1.4
|
||||
|
@ -203,6 +242,17 @@ public class WebClientResponseException extends WebClientException {
|
|||
return this.request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a function to find a decoder the given target type.
|
||||
* For use with {@link #getResponseBodyAs(Class)}.
|
||||
* @param decoderFunction the function to find a decoder with
|
||||
* @since 6.0
|
||||
*/
|
||||
public void setBodyDecodeFunction(Function<ResolvableType, ?> decoderFunction) {
|
||||
this.bodyDecodeFunction = decoderFunction;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create {@code WebClientResponseException} or an HTTP status specific subclass.
|
||||
* @since 5.1
|
||||
|
|
|
@ -103,9 +103,11 @@
|
|||
<suppress files="org[\\/]springframework[\\/]web[\\/]bind[\\/]annotation[\\/]ValueConstants" checks="InterfaceIsType"/>
|
||||
<suppress files="PatternParseException" checks="JavadocVariable"/>
|
||||
<suppress files="web[\\/]reactive[\\/]socket[\\/]CloseStatus" checks="JavadocStyle"/>
|
||||
<suppress files="RestClientResponseException" checks="MutableException"/>
|
||||
|
||||
<!-- spring-webflux -->
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]web[\\/]reactive[\\/]resource[\\/]GzipSupport" checks="IllegalImport" id="bannedJUnitJupiterImports"/>
|
||||
<suppress files="WebClientResponseException" checks="MutableException"/>
|
||||
|
||||
<!-- spring-webmvc -->
|
||||
<suppress files="org[\\/]springframework[\\/]web[\\/]servlet[\\/]tags[\\/]form[\\/].*Tag" checks="JavadocVariable"/>
|
||||
|
|
Loading…
Reference in New Issue