Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the most likely cause of exceptions mysteriously escaping a try-catch block in this case?

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:

enter image description here

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.

like image 489
Martin Drozdik Avatar asked Dec 23 '22 12:12

Martin Drozdik


2 Answers

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.

like image 153
Utku Özdemir Avatar answered Dec 28 '22 07:12

Utku Özdemir


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.

like image 32
Sébastien Deleuze Avatar answered Dec 28 '22 07:12

Sébastien Deleuze