Skip to content

Commit 922636e

Browse files
committed
Content decoding in client exceptions
Closes gh-28190
1 parent 6479566 commit 922636e

File tree

7 files changed

+208
-8
lines changed

7 files changed

+208
-8
lines changed

spring-web/src/main/java/org/springframework/http/ProblemDetail.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ protected ProblemDetail(ProblemDetail other) {
7474
this.instance = other.instance;
7575
}
7676

77+
/**
78+
* For deserialization.
79+
*/
80+
protected ProblemDetail() {
81+
}
82+
7783

7884
/**
7985
* Variant of {@link #setType(URI)} for chained initialization.

spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,26 @@
1616

1717
package org.springframework.web.client;
1818

19+
import java.io.ByteArrayInputStream;
1920
import java.io.IOException;
21+
import java.io.InputStream;
2022
import java.nio.charset.Charset;
2123
import java.nio.charset.StandardCharsets;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.function.Function;
2227

28+
import org.springframework.core.ResolvableType;
2329
import org.springframework.core.log.LogFormatUtils;
2430
import org.springframework.http.HttpHeaders;
2531
import org.springframework.http.HttpStatus;
2632
import org.springframework.http.HttpStatusCode;
2733
import org.springframework.http.MediaType;
2834
import org.springframework.http.client.ClientHttpResponse;
35+
import org.springframework.http.converter.HttpMessageConverter;
2936
import org.springframework.lang.Nullable;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.CollectionUtils;
3039
import org.springframework.util.FileCopyUtils;
3140
import org.springframework.util.ObjectUtils;
3241

@@ -50,6 +59,20 @@
5059
*/
5160
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
5261

62+
@Nullable
63+
private List<HttpMessageConverter<?>> messageConverters;
64+
65+
66+
/**
67+
* For internal use from the RestTemplate, to pass the message converters
68+
* to use to decode error content.
69+
* @since 6.0
70+
*/
71+
void setMessageConverters(List<HttpMessageConverter<?>> converters) {
72+
this.messageConverters = Collections.unmodifiableList(converters);
73+
}
74+
75+
5376
/**
5477
* Delegates to {@link #hasError(HttpStatusCode)} with the response status code.
5578
* @see ClientHttpResponse#getStatusCode()
@@ -155,15 +178,48 @@ protected void handleError(ClientHttpResponse response, HttpStatusCode statusCod
155178
Charset charset = getCharset(response);
156179
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
157180

181+
RestClientResponseException ex;
158182
if (statusCode.is4xxClientError()) {
159-
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
183+
ex = HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
160184
}
161185
else if (statusCode.is5xxServerError()) {
162-
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
186+
ex = HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
163187
}
164188
else {
165-
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
189+
ex = new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
166190
}
191+
192+
if (!CollectionUtils.isEmpty(this.messageConverters)) {
193+
ex.setBodyConvertFunction(initBodyConvertFunction(response, body));
194+
}
195+
196+
throw ex;
197+
}
198+
199+
/**
200+
* Return a function for decoding the error content. This can be passed to
201+
* {@link RestClientResponseException#setBodyConvertFunction(Function)}.
202+
* @since 6.0
203+
*/
204+
protected Function<ResolvableType, ?> initBodyConvertFunction(ClientHttpResponse response, byte[] body) {
205+
Assert.state(!CollectionUtils.isEmpty(this.messageConverters), "Expected message converters");
206+
return resolvableType -> {
207+
try {
208+
HttpMessageConverterExtractor<?> extractor =
209+
new HttpMessageConverterExtractor<>(resolvableType.getType(), this.messageConverters);
210+
211+
return extractor.extractData(new ClientHttpResponseDecorator(response) {
212+
@Override
213+
public InputStream getBody() {
214+
return new ByteArrayInputStream(body);
215+
}
216+
});
217+
}
218+
catch (IOException ex) {
219+
throw new RestClientException(
220+
"Error while extracting response for type [" + resolvableType + "]", ex);
221+
}
222+
};
167223
}
168224

169225
/**

spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
import java.io.UnsupportedEncodingException;
2020
import java.nio.charset.Charset;
2121
import java.nio.charset.StandardCharsets;
22+
import java.util.function.Function;
2223

24+
import org.springframework.core.ParameterizedTypeReference;
25+
import org.springframework.core.ResolvableType;
2326
import org.springframework.http.HttpHeaders;
2427
import org.springframework.http.HttpStatusCode;
2528
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
2630

2731
/**
2832
* Common base class for exceptions that contain actual HTTP response data.
@@ -49,6 +53,9 @@ public class RestClientResponseException extends RestClientException {
4953
@Nullable
5054
private final String responseCharset;
5155

56+
@Nullable
57+
private Function<ResolvableType, ?> bodyConvertFunction;
58+
5259

5360
/**
5461
* Construct a new instance of with the given response data.
@@ -153,4 +160,43 @@ public String getResponseBodyAsString(Charset fallbackCharset) {
153160
}
154161
}
155162

163+
/**
164+
* Convert the error response content to the specified type.
165+
* @param targetType the type to convert to
166+
* @param <E> the expected target type
167+
* @return the converted object, or {@code null} if there is no content
168+
* @since 6.0
169+
*/
170+
@Nullable
171+
public <E> E getResponseBodyAs(Class<E> targetType) {
172+
return getResponseBodyAs(ResolvableType.forClass(targetType));
173+
}
174+
175+
/**
176+
* Variant of {@link #getResponseBodyAs(Class)} with
177+
* {@link ParameterizedTypeReference}.
178+
* @since 6.0
179+
*/
180+
@Nullable
181+
public <E> E getResponseBodyAs(ParameterizedTypeReference<E> targetType) {
182+
return getResponseBodyAs(ResolvableType.forType(targetType.getType()));
183+
}
184+
185+
@SuppressWarnings("unchecked")
186+
@Nullable
187+
private <E> E getResponseBodyAs(ResolvableType targetType) {
188+
Assert.state(this.bodyConvertFunction != null, "Function to convert body not set");
189+
return (E) this.bodyConvertFunction.apply(targetType);
190+
}
191+
192+
/**
193+
* Provide a function to use to decode the response error content
194+
* via {@link #getResponseBodyAs(Class)}.
195+
* @param bodyConvertFunction the function to use
196+
* @since 6.0
197+
*/
198+
public void setBodyConvertFunction(Function<ResolvableType, ?> bodyConvertFunction) {
199+
this.bodyConvertFunction = bodyConvertFunction;
200+
}
201+
156202
}

spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -195,6 +195,7 @@ else if (kotlinSerializationJsonPresent) {
195195
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
196196
}
197197

198+
updateErrorHandlerConverters();
198199
this.uriTemplateHandler = initUriTemplateHandler();
199200
}
200201

@@ -219,9 +220,16 @@ public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
219220
validateConverters(messageConverters);
220221
this.messageConverters.addAll(messageConverters);
221222
this.uriTemplateHandler = initUriTemplateHandler();
223+
updateErrorHandlerConverters();
222224
}
223225

224226

227+
private void updateErrorHandlerConverters() {
228+
if (this.errorHandler instanceof DefaultResponseErrorHandler handler) {
229+
handler.setMessageConverters(this.messageConverters);
230+
}
231+
}
232+
225233
private static DefaultUriBuilderFactory initUriTemplateHandler() {
226234
DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory();
227235
uriFactory.setEncodingMode(EncodingMode.URI_COMPONENT); // for backwards compatibility..
@@ -240,6 +248,7 @@ public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters
240248
this.messageConverters.clear();
241249
this.messageConverters.addAll(messageConverters);
242250
}
251+
updateErrorHandlerConverters();
243252
}
244253

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

267277
/**

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,15 +22,19 @@
2222
import java.util.Map;
2323
import java.util.Optional;
2424
import java.util.OptionalLong;
25+
import java.util.function.Function;
2526
import java.util.function.Supplier;
2627

2728
import reactor.core.publisher.Flux;
2829
import reactor.core.publisher.Mono;
2930

3031
import org.springframework.core.ParameterizedTypeReference;
32+
import org.springframework.core.ResolvableType;
33+
import org.springframework.core.codec.Decoder;
3134
import org.springframework.core.codec.Hints;
3235
import org.springframework.core.io.buffer.DataBuffer;
3336
import org.springframework.core.io.buffer.DataBufferUtils;
37+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
3438
import org.springframework.http.HttpHeaders;
3539
import org.springframework.http.HttpRequest;
3640
import org.springframework.http.HttpStatus;
@@ -39,8 +43,11 @@
3943
import org.springframework.http.ResponseCookie;
4044
import org.springframework.http.ResponseEntity;
4145
import org.springframework.http.client.reactive.ClientHttpResponse;
46+
import org.springframework.http.codec.DecoderHttpMessageReader;
4247
import org.springframework.http.codec.HttpMessageReader;
4348
import org.springframework.http.server.reactive.ServerHttpResponse;
49+
import org.springframework.lang.Nullable;
50+
import org.springframework.util.Assert;
4451
import org.springframework.util.MimeType;
4552
import org.springframework.util.MultiValueMap;
4653
import org.springframework.web.reactive.function.BodyExtractor;
@@ -201,11 +208,15 @@ public Mono<WebClientResponseException> createException() {
201208
.defaultIfEmpty(EMPTY)
202209
.onErrorReturn(ex -> !(ex instanceof Error), EMPTY)
203210
.map(bodyBytes -> {
211+
204212
HttpRequest request = this.requestSupplier.get();
205-
Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null);
213+
Optional<MediaType> mediaType = headers().contentType();
214+
Charset charset = mediaType.map(MimeType::getCharset).orElse(null);
206215
HttpStatusCode statusCode = statusCode();
216+
217+
WebClientResponseException exception;
207218
if (statusCode instanceof HttpStatus httpStatus) {
208-
return WebClientResponseException.create(
219+
exception = WebClientResponseException.create(
209220
statusCode,
210221
httpStatus.getReasonPhrase(),
211222
headers().asHttpHeaders(),
@@ -214,16 +225,35 @@ public Mono<WebClientResponseException> createException() {
214225
request);
215226
}
216227
else {
217-
return new UnknownHttpStatusCodeException(
228+
exception = new UnknownHttpStatusCodeException(
218229
statusCode,
219230
headers().asHttpHeaders(),
220231
bodyBytes,
221232
charset,
222233
request);
223234
}
235+
exception.setBodyDecodeFunction(initDecodeFunction(bodyBytes, mediaType.orElse(null)));
236+
return exception;
224237
});
225238
}
226239

240+
private Function<ResolvableType, ?> initDecodeFunction(byte[] body, @Nullable MediaType contentType) {
241+
return targetType -> {
242+
Decoder<?> decoder = null;
243+
for (HttpMessageReader<?> reader : strategies().messageReaders()) {
244+
if (reader.canRead(targetType, contentType)) {
245+
if (reader instanceof DecoderHttpMessageReader<?> decoderReader) {
246+
decoder = decoderReader.getDecoder();
247+
break;
248+
}
249+
}
250+
}
251+
Assert.state(decoder != null, "No suitable decoder");
252+
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(body);
253+
return decoder.decode(buffer, targetType, null, Collections.emptyMap());
254+
};
255+
}
256+
227257
@Override
228258
public <T> Mono<T> createError() {
229259
return createException().flatMap(Mono::error);

0 commit comments

Comments
 (0)