Skip to content

Commit

Permalink
Content decoding in client exceptions
Browse files Browse the repository at this point in the history
Closes gh-28190
  • Loading branch information
rstoyanchev committed May 10, 2022
1 parent 6479566 commit 922636e
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ protected ProblemDetail(ProblemDetail other) {
this.instance = other.instance;
}

/**
* For deserialization.
*/
protected ProblemDetail() {
}


/**
* Variant of {@link #setType(URI)} for chained initialization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -50,6 +59,20 @@
*/
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()
Expand Down Expand Up @@ -155,15 +178,48 @@ protected void handleError(ClientHttpResponse response, HttpStatusCode statusCod
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);
}
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
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.
Expand All @@ -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.
Expand Down Expand Up @@ -153,4 +160,43 @@ public String getResponseBodyAsString(Charset fallbackCharset) {
}
}

/**
* 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;
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -195,6 +195,7 @@ else if (kotlinSerializationJsonPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}

updateErrorHandlerConverters();
this.uriTemplateHandler = initUriTemplateHandler();
}

Expand All @@ -219,9 +220,16 @@ public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
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..
Expand All @@ -240,6 +248,7 @@ public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters
this.messageConverters.clear();
this.messageConverters.addAll(messageConverters);
}
updateErrorHandlerConverters();
}

private void validateConverters(List<HttpMessageConverter<?>> messageConverters) {
Expand All @@ -262,6 +271,7 @@ public List<HttpMessageConverter<?>> getMessageConverters() {
public void setErrorHandler(ResponseErrorHandler errorHandler) {
Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
this.errorHandler = errorHandler;
updateErrorHandlerConverters();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -22,15 +22,19 @@
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;
Expand All @@ -39,8 +43,11 @@
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;
Expand Down Expand Up @@ -201,11 +208,15 @@ public Mono<WebClientResponseException> createException() {
.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(),
Expand All @@ -214,16 +225,35 @@ public Mono<WebClientResponseException> createException() {
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);
Expand Down
Loading

0 comments on commit 922636e

Please sign in to comment.