-
Notifications
You must be signed in to change notification settings - Fork 804
Description
Reproducable with: Kotlin and Java
Java version: 11
Spring Boot version: 2.6.3
Spring Cloud version: 2021.0.0
Client: OpenFeign
I am migrating one of my spring boot projects from hystrix to r4j and noticed a difference in the exception propagation in comparision to hystrix. I really don't know whether this is an intended behaviour, nor whether it belongs to openfeign, r4j or spring :(.
My FallbackFactory throws my CustomerNotFoundException
:
@Component
class CustomerClientFallbackFactory : FallbackFactory<CustomerClient> {
override fun create(cause: Throwable): CustomerClient =
object : CustomerClient {
override fun test() {
// Why will this exception end up in an IllegalStateException?
throw CustomerNotFoundException()
}
}
}
class CustomerNotFoundException : BusinessException("not found")
My feign client uses this FallbackFactory:
@FeignClient(
name = "customer",
url = "http://localhost:8888",
configuration = [CustomerClientConfig::class],
fallbackFactory = CustomerClientFallbackFactory::class
)
interface CustomerClient {
@RequestLine("GET /")
fun test()
}
To simulate and illustrate the behaviour I have created two endpoints with the same client:
@RestController
class CustomerController(
private val customerClient: CustomerClient
) {
private val log = KotlinLogging.logger {}
@GetMapping("/wrap")
fun wrap() {
try {
customerClient.test()
} catch (e: Exception) {
log.info { "Exception: " + e.javaClass }
// Output: Exception: class java.lang.IllegalStateException
log.info(e) {}
}
}
@GetMapping("/unwrap")
fun unwrap() {
try {
unwrapFeignCircuitBreakerExceptions {
customerClient.test()
}
} catch (e: Exception) {
log.info { "Exception: " + e.javaClass }
// Output: Exception: class org.example.auth.CustomerNotFoundException
}
}
}
My expectation is that wrap()
catches CustomerNotFoundException
- but this is not the case. Instead I get a IllegalStateException
which wraps my CustomerNotFoundException
:
java.lang.IllegalStateException: java.lang.reflect.InvocationTargetException
at org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler.lambda$invoke$0(FeignCircuitBreakerInvocationHandler.java:99) ~[spring-cloud-openfeign-core-3.1.0.jar:3.1.0]
at io.vavr.control.Try.lambda$recover$6ea7267f$1(Try.java:949) ~[vavr-0.10.2.jar:na]
at io.vavr.control.Try.of(Try.java:75) ~[vavr-0.10.2.jar:na]
at io.vavr.control.Try.recover(Try.java:949) ~[vavr-0.10.2.jar:na]
at org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreaker.run(Resilience4JCircuitBreaker.java:123) ~[spring-cloud-circuitbreaker-resilience4j-2.1.0.jar:2.1.0]
at org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler.invoke(FeignCircuitBreakerInvocationHandler.java:102) ~[spring-cloud-openfeign-core-3.1.0.jar:3.1.0]
at com.sun.proxy.$Proxy77.test(Unknown Source) ~[na:na]
at org.example.auth.CustomerController.wrap(CustomerController.kt:18) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.15.jar:5.3.15]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.15.jar:5.3.15]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.15.jar:5.3.15]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.56.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.15.jar:5.3.15]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.56.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.15.jar:5.3.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.15.jar:5.3.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.15.jar:5.3.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.15.jar:5.3.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.15.jar:5.3.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.15.jar:5.3.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
Caused by: java.lang.reflect.InvocationTargetException: null
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.cloud.openfeign.FeignCircuitBreakerInvocationHandler.lambda$invoke$0(FeignCircuitBreakerInvocationHandler.java:96) ~[spring-cloud-openfeign-core-3.1.0.jar:3.1.0]
... 57 common frames omitted
Caused by: org.example.auth.CustomerNotFoundException: not found
at org.example.auth.CustomerClientFallbackFactory$create$1.test(CustomerClientFallbackFactory.kt:12) ~[main/:na]
... 62 common frames omitted
unwrap()
uses an infamous lambda unwrapFeignCircuitBreakerExceptions
(found on the internet) which unwraps the exception chain and gives me the desired result. Which is of course far from ideal because I have to wrap every client call with unnecessary boilerplate code. Furthermore it looks like a hack or workaround:
inline fun <T> unwrapFeignCircuitBreakerExceptions(callable: () -> T): T {
return try {
callable.invoke()
} catch (ex: InvocationTargetException) {
throw ex.cause ?: ex
} catch (ex: NoFallbackAvailableException) {
throw ex.cause ?: ex
} catch (ex: IllegalStateException) {
throw (ex.cause?.cause ?: ex.cause) ?: ex
}
}
Is there an official way to handle/configure this? I mean it is a valid use-case to throw an exception in some fallback cases instead of providing a fallback object. Or is this a bug - as this behaviour was not present in combination with hystrix?
Minimal, complete, verifiable example project: https://github.com/shellrausch/feign-r4j-fallback
Thanks!