Affects: 6.1.1
It was not immediately clear to me, that custom exceptions cannot be any of the following types:
UncheckedIOException
IOException
HttpMessageNotReadableException
If for example you define an exception as:
public class MyException extends IOException { }
// or
public class MyException extends UncheckedIOException { }
And attempt to throw it in a status handler:
RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
try {
restClient.get()
.uri("/products/{id}", 99999999)
.retrieve()
.onStatus(HttpStatusCode::isError, (req, resp) -> {
throw new MyException();
})
.body(Product.class);
} catch (MyException ex) {
// does not reach here
}
Does not work.
Instead a RestClientException
is thrown because the (3) exceptions I listed above are explicitly caught here and as a result, to access your custom exception, you have to unwrap it:
RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
try {
restClient.get()
.uri("/products/{id}", 99999999)
.retrieve()
.onStatus(HttpStatusCode::isError, (req, resp) -> {
throw new MyException();
})
.body(Product.class);
} catch (RestClientException ex) {
MyException myEx = (MyException) ex.getRootCause();
}
The same also applies when using defaultStatusHandler
when building the RestClient
as well.
It would be nice to mention somewhere in the documentation and Javadoc that custom exceptions cannot be either of the (3) types above nor extend those types in some way otherwise a RestClientException
will be thrown, not your custom exception.
Minimal Spring Boot example
package com.example.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import java.io.IOException;
import java.util.Objects;
@SpringBootApplication
public class DemoApplication {
private static final Logger logger = LoggerFactory.getLogger(DemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@EventListener(ApplicationReadyEvent.class)
public void verifyTodoService(ApplicationReadyEvent event) {
var todoService = event.getApplicationContext().getBean(TodoService.class);
try {
todoService.getProduct(99999999);
} catch (MyException ex) {
logger.error("verifyTodoService :: this should happen", ex);
} catch (RestClientException ex) {
logger.error("verifyTodoService :: this should not happen");
Objects.requireNonNull((MyException) ex.getRootCause());
logger.error("unwrapped custom exception successfully");
}
}
@EventListener(ApplicationReadyEvent.class)
public void verifyRestClient(ApplicationReadyEvent event) {
RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
try {
restClient.get()
.uri("/products/{id}", 99999999)
.retrieve()
.onStatus(HttpStatusCode::isError, (req, resp) -> {
throw new MyException();
})
.body(Product.class);
} catch (Exception ex) {
logger.error("verifyRestClient listener :: Caught exception is MyException? {}", MyException.class.isAssignableFrom(ex.getClass()));
}
}
@Bean
public TodoService todoService(RestClient.Builder restClientBuilder) {
RestClient restClient = restClientBuilder
.baseUrl("https://dummyjson.com")
.defaultStatusHandler(HttpStatusCode::isError, (req, res) -> {
throw new MyException();
})
.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(TodoService.class);
}
class MyException extends IOException {
}
record Product(String id, String title, String description) {
}
interface TodoService {
@GetExchange("/products/{id}")
Product getProduct(@PathVariable int id) throws MyException;
}
}