Repository: square/retrofit
Issue: UndeclaredThrowableException when using single instance of Retrofit with non-IOException in custom CallAdapter
Labels: Bug

Retrofit crashes with `UndeclaredThrowableException` when single instance of Retrofit is used with custom adapter which returns non-`IOException`.

Simple code to reproduce. The code inside `while` loop will eventually fail.

```
private val service by lazy {
    val client = OkHttpClient.Builder()
        .build()

    val retrofit = Retrofit.Builder()
        .client(client)
        .baseUrl("https://unresolved-host.com/")
        .addCallAdapterFactory(RetrofitExceptionHandlerAdapterFactory())
        .build()
    retrofit.create(RepoService::class.java)
}

while (true) {
    viewModelScope.launch {
        try {
            service.searchRepos()
        } catch (e: DataException.DataNotFoundException) {
            println("KOKO $e")
        }
    }
}

interface RepoService {
    @GET("search/repositories")
    suspend fun searchRepos()
}

sealed class DataException(override val cause: Throwable? = null): Throwable(cause) {
    data class DataNotFoundException(override val cause: Throwable? = null): DataException(cause)
}
```
In custom adapter, instead of returning `IOException` I return `DataNotFoundException`( non-`IOException`) in `onFailure` method.

```
class RetrofitExceptionHandlerAdapterFactory: CallAdapter.Factory() {
    override fun get(
        returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // Validate Call
        val typeCall = getRawType(returnType)
        if (typeCall != Call::class.java)
            return null

        val responseType = getParameterUpperBound(0, returnType as ParameterizedType)
        val responseTypeClass = getRawType(typeCall)
        return RetrofitExceptionHandlerAdapter(responseType, responseTypeClass)
    }
}


class RetrofitExceptionHandlerAdapter<T>(
    private val responseType: Type,
    private val responseTypeClass: Class<T>
) : CallAdapter<T, Call<T>> {

    override fun responseType(): Type = responseType

    private fun responseTypeClass(): Class<T> = responseTypeClass

    override fun adapt(call: Call<T>): BodyCall<T> = BodyCall(call, responseTypeClass())

    class BodyCall<T>(
        private val delegate: Call<T>,
        private val responseClass: Class<T>
    ) : Call<T> {
        override fun enqueue(callback: Callback<T>) {
            delegate.enqueue(
                object : Callback<T> {
                    override fun onResponse(call: Call<T>, response: Response<T>) {
                        callback.onResponse(call, response)
                    }

                    override fun onFailure(call: Call<T>, t: Throwable) {
                        callback.onFailure(call, DataException.DataNotFoundException())
                    }
                })
        }

        override fun isExecuted() = delegate.isExecuted

        // Since suspend functions won't use this method, no need to implement
        override fun execute(): Response<T> = throw NotImplementedError("Method not implemented")
        override fun cancel() = delegate.cancel()
        override fun isCanceled() = delegate.isCanceled
        override fun clone(): Call<T> = BodyCall(delegate.clone(), responseClass)
        override fun request(): Request = delegate.request()
        override fun timeout(): Timeout = delegate.timeout()
    }
}
```



**Solution:**
1. Either create new instance of retrofit on every call (which would not make sense)
2. Or inherit `DataNotFoundException` from `IOException` instead of `Throwable`

**Retrofit version:** `2.9.0`

Issue comments:
 interface RepoService {
    @GET("search/repositories")
    @Throws(DataException.DataNotFoundException::class)
    suspend fun searchRepos()
  }

add @Throws(DataException.DataNotFoundException::class) to function then the DataNotFoundException is declared 
so when exception is thrown to kotlin Coroutine，it won't be throw as UndeclaredThrowableException 
Is there a reason that you extend from `Throwable` instead of `Exception`?

Pull request review comments:

Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit2/HttpServiceMethod.java b/retrofit/src/main/java/retrofit2/HttpServiceMethod.java
index 2ca5c7f0ae..41758a9cd0 100644
--- a/retrofit/src/main/java/retrofit2/HttpServiceMethod.java
+++ b/retrofit/src/main/java/retrofit2/HttpServiceMethod.java
@@ -243,7 +243,10 @@ protected Object adapt(Call<ResponseT> call, Object[] args) {
         } else {
           return KotlinExtensions.await(call, continuation);
         }
-      } catch (Exception e) {
+      } catch (VirtualMachineError | ThreadDeath | LinkageError e) {
+        // Do not attempt to capture fatal throwables. This list is derived RxJava's `throwIfFatal`.
+        throw e;
+      } catch (Throwable e) {
         return KotlinExtensions.suspendAndThrow(e, continuation);
       }
     }
diff --git a/retrofit/src/main/java/retrofit2/KotlinExtensions.kt b/retrofit/src/main/java/retrofit2/KotlinExtensions.kt
index d334b25136..8dc4265309 100644
--- a/retrofit/src/main/java/retrofit2/KotlinExtensions.kt
+++ b/retrofit/src/main/java/retrofit2/KotlinExtensions.kt
@@ -116,7 +116,7 @@ suspend fun <T> Call<T>.awaitResponse(): Response<T> {
  * The implementation is derived from:
  * https://github.com/Kotlin/kotlinx.coroutines/pull/1667#issuecomment-556106349
  */
-internal suspend fun Exception.suspendAndThrow(): Nothing {
+internal suspend fun Throwable.suspendAndThrow(): Nothing {
   suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
     Dispatchers.Default.dispatch(continuation.context) {
       continuation.intercepted().resumeWithException(this@suspendAndThrow)


Test patch: