I am currently working with the new RestClient in Spring Boot 3.2/Spring Framework 5.1, and I have encountered a challenge with handling 404 errors. My goal is to gracefully handle these errors without causing subsequent steps in my code to fail, particularly when converting the response body.
Example:
class CmsClient {
pubic Policy getPolicy() {
return restClient.get()
.uri("some/url")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(ignore404())
.body(Policy.class);
}
public static ResponseErrorHandler ignore404() {
return new ResponseErrorHandler() {
@Override
public boolean hasError(final ClientHttpResponse response) throws IOException {
return response.getStatusCode() == HttpStatus.NOT_FOUND;
}
@Override
public void handleError(final ClientHttpResponse response) {
//Do nothing
}
};
}
}
This is the exception I get:
org.springframework.web.client.RestClientException: Error while extracting response for type [package.Policy] and content type [application/json]
at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:216)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.readBody(DefaultRestClient.java:641)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.body(DefaultRestClient.java:589)
at package.CmsClient.getPolicy(CmsClient.java:27)
at package.CmsClientTest.getPolicy_404_ThrowException_Test(CmsClientTest.java:61)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: No content to map due to end-of-input
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354)
at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:200)
... 7 more
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 0]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1752)
at com.fasterxml.jackson.databind.ObjectReader._initForReading(ObjectReader.java:360)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2095)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1481)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:395)
... 9 more
I anticipate that the method .body(Policy.class);
should return null instead of triggering an exception due to a MismatchedInputException. This behavior aligned with what was previously observed in WebClient, and I was expecting a similar approach in this context.
Current Solution: As a temporary measure, one can specify String.class in the body method call. However, this approach compromises the fluidity and ease of the code, necessitating additional handling with ObjectMapper and similar tools.
I solved it by registering a custom message converter:
public RestClient create(String baseUrl, Duration timeout) {
return RestClient.builder()
.baseUrl("...")
//addFirst is important
.messageConverters(converterList -> converterList.addFirst(new JsonMessageConverter()))
.build();
}
This is my custom message converter:
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
class JsonMessageConverter extends MappingJackson2HttpMessageConverter {
@Override
public Object read(@NotNull Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
var bytes = inputMessage.getBody().readAllBytes();
if (bytes.length == 0) {
return null;
}
return super.read(type, contextClass, new HttpInputMessageWrapper(new ByteArrayInputStream(bytes), inputMessage.getHeaders()));
}
private static class HttpInputMessageWrapper implements HttpInputMessage {
private final InputStream body;
private final HttpHeaders headers;
private HttpInputMessageWrapper(InputStream body, HttpHeaders headers) {
this.body = body;
this.headers = headers;
}
@Override
public @NotNull InputStream getBody() {
return this.body;
}
@Override
public @NotNull HttpHeaders getHeaders() {
return this.headers;
}
}
}
And then, I add the error handler I posted in the question to handle 404 gracefully:
Optional<Policy> fetchDataRemotly() {
var policy = restClient.get()
.uri("...")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(RestClientFactory.ignore404())
.body(Policy.class);
/*
* Variable 'policy' will be null if we get 404.
*/
return Optional.ofNullable(policy);
}