I am using a Spring WebClient in a Kotlin project like this:
data class DTO(val name: String)
@Component
class Runner: ApplicationRunner
{
override fun run(args: ApplicationArguments?)
{
try
{
val dto = get<DTO>()
}
catch (e: Exception)
{
println("ERROR, all exceptions should have been caught in 'get' ")
}
}
}
inline private fun<reified TResult: Any> get(): TResult?
{
var result: TResult? = null
try
{
result = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting")
.get()
.retrieve()
.bodyToMono<TResult>()
.block()
}
catch (e: Exception)
{
println("WORKS AS EXPECTED!!")
}
return result
}
The client will throw an exception, because the API will return a 404. However the exception is not caught where it should be, namely in the body of the get
function, but it is propagated to the outer exception handler.
It is interesting to note that this happens only if the exception is thrown by the WebClient
. If I replace the code in the try
clause with a simple throw Exception("error")
, the exception is caught where it should be.
Similarly, when I change the signature of get
to a non-generic inline private fun get(): DTO?
the problem also goes away.
For an exception to escape the try-catch
block seems like a fundamental bug in the Kotlin tools. On the other hand, the fact that this happens only with the WebClient
class indicates that this is a Spring problem. Or, it may be just me, using the tools in a wrong way.
I am really baffled here and have no idea how to proceed. Any ideas on why this might be happening are most welcome. Just for completeness, this is what it looks like in the debugger:
EDIT
The issue goes away after upgrading Spring Boot to 2.0.0.M6, it is still present in M5.
So it seems that this was a Spring issue and not a Kotlin issue. On the other hand it would be still nice to understand how a library that you include can seemingly cause the program to violate the laws of the programming language it is written in.
I tried the code with Spring Boot version 2.0.0.M5
and 2.0.0.M6
, and it seems the behavior of the following block is different between those 2 versions:
result = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting")
.get()
.retrieve()
.bodyToMono<TResult>()
.block()
somewhere along the chain, on Spring Boot 2.0.0.M5
, the WebClientResponseException
is returned, on Spring Boot 2.0.0.M6
it is thrown.
If you add a e.printStackTrace()
to your outer catch, you will notice that the stack trace is:
java.lang.ClassCastException: org.springframework.web.reactive.function.client.WebClientResponseException cannot be cast to com.example.demo.DTO at com.example.demo.Runner.run(Test.kt:18) at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:780) at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:770) at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:760) at org.springframework.boot.SpringApplication.run(SpringApplication.java:328) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1245) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1233) at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:10)
So, actually, problem is, the returned WebClientResponseException
is tried to be cast to DTO
class on the moment of return of the call val dto = get<DTO>()
. This means that, when you assign result = ...
, there is no type checking done yet. So, if you change your code to, for example, call get<Object>()
instead of get<DTO>()
, it won't hit any catch blocks.
If you convert it to bytecode in IntelliJ Idea, and then decompile it to Java, you can see this block:
public class Runner implements ApplicationRunner {
public void run(@Nullable ApplicationArguments args) {
try {
Object result$iv = null;
try {
ResponseSpec $receiver$iv$iv = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting").get().retrieve();
Mono var10000 = $receiver$iv$iv.bodyToMono((ParameterizedTypeReference)(new Runner$run$$inlined$get$1()));
Intrinsics.checkExpressionValueIsNotNull(var10000, "bodyToMono(object : Para…zedTypeReference<T>() {})");
result$iv = var10000.block();
} catch (Exception var7) {
String var5 = "WORKS AS EXPECTED!!";
System.out.println(var5);
}
DTO var2 = (DTO)result$iv;
} catch (Exception var8) {
String var3 = "ERROR, all exceptions should have been caught in 'get' ";
System.out.println(var3);
}
}
}
Here you can notice that casting to DTO is done on the point of method return (which is not a return anymore because it is inlined), after the inner catch block: DTO var2 = (DTO)result$iv;
. It seems like that's the behavior for the inlined methods with reified type parameters.
This is due to SPR-16025 (see related commit) since the Kotlin extension is using internally the ParameterizedTypeReference
variant, which has been fixed in Spring Framework 5.0.1, and transitively in Spring Boot 2.0.0.M6.
Note than if you use bodyToMono(TResult::class.java)
with Spring Boot 2.0.0.M5, it will works as expected.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With