Ktor seems to be swallowing exceptions

Viewed 2679

I am using Ktor with kotlin multiplatform and I am trying to figure out why I am not getting any exceptions that get thrown. In my client configuration I am using the HttpResonseValidator to check the status code coming back

private val client = HttpClient(clientEngine) {
        install(JsonFeature) {
            serializer = KotlinxSerializer(Json.nonstrict)
        }
//        addDefaultResponseValidation()
        HttpResponseValidator{

            validateResponse { response: HttpResponse ->
                val statusCode = response.status.value
                when (statusCode) {
                    in 300..399 -> throw RedirectResponseException(response)
                    in 400..499 -> throw ClientRequestException(response)
                    in 500..599 -> throw ServerResponseException(response)
                }

                if (statusCode >= 600) {
                    throw ResponseException(response)
                }
            }

            handleResponseException { cause: Throwable ->
                throw cause
            }
        }
    }

I am returning http status code 401 error on my server for testing so I should see my code throw a ClientRequestException and the validateResponse does get called but I never see any exceptions in the console and my app just stops without any indication anything happened.

this is my call

private suspend fun getDataForUrl(url:String, callback: (MyObject?) -> Unit){
    val response = client.get<MyObject>(url)
    callback(response)
}

Which is called via

fun getData(callback: (MyObject?) -> Unit){
    launch(dispatcher()){
        getDataForUrl("$BASE_URL", callback)
    }
}

When I surround the call with a try/catch

try{
    val response = client.get<MyObject>(url)
catch(e:Exception){
}

I do get the exception but I dont really like that its getting caught here and not in the upper levels of my code.

Why is it getting swallowed up when there isn't a try/catch around it?

2 Answers

I had this FATAL EXCEPTION in my Kotlin multiplatform project too.

My Ktor method looks like this:

suspend fun requestToken(userName: String, password: String): AuthResponse {
    return client.post {
        url(urlOauth)
        accept(ContentType.Application.Json)
        body = FormDataContent(Parameters.build {
            append("grant_type", "password")
            append("username", userName)
            append("password", password)
        })
    }
}

The HttpResponseValidator is used if everything works fine. But when I force an error, e.g., with a wrong URL, the HttpResponseValidator isn't used, and my app, in this case an iOS app, crashes.

When I open the generated Swift class, I see this comment added to my method:

/**
 @note This method converts instances of CancellationException to errors.
 Other uncaught Kotlin exceptions are fatal.
*/

This means that all other errors are crashing my app. The CancellationException has something to do with the suspend.

To prevent the crash and get the error, I added @Throws(CancellationException::class, ResponseException::class). The method now looks like this:

@Throws(CancellationException::class, ResponseException::class)
suspend fun requestToken(userName: String, password: String): AuthResponse {
    return client.post {
        url(urlOauth)
        accept(ContentType.Application.Json)
        body = FormDataContent(Parameters.build {
            append("grant_type", "password")
            append("username", userName)
            append("password", password)
        })
    }
}

This is how I use it:

ApiKt.requestToken(userName: "xxx", password: "xxx") { (response: AuthResponse?, error: Error?) in
    if let response = response {
        print("access token: \(response.accessToken)")
    } else if let error = error {
        print("failed to load access token. error: \(error)")
    } else {
        print("Something strange has happened. We have received neither a response nor an error.")
    }
}

When I now force an error, the app doesn't crash. Instead, I get an error and print it out (in this case).

Finally, I don't know why the HttpResponseValidator isn't used in case of an error, but this prevents the FATAL EXCEPTION.

I encountered a similar issue where my custom HttpResponseValidator was called just fine for response status codes other than 401. After further investigation I found out, that my breakpoint in DefaultResponseValidation.kt (as used by addDefaultResponseValidation) was triggered instead. I do not quite understant why validateResponse() was triggered at all as addDefaultResponseValidation() is never called in my code. But maybe this is the default when using the default Http Client. However, the idea that status code 401 would trigger some default code prior in the execution change (that would return before my validator was called), led me to trying out

expectSuccess = false

which solved the issue for me.

Related