Search code examples
ktorkotlin-multiplatform

Kotlin Multiplatform - Handle response http code and exceptions with Ktor


I Just started to explore KMM, which seems really nice so far.

Basically, what I'm trying to to is to handle all http error code (like 401, 404, 500) globally, in one place.

However, I'm not sure how to interact with the response.

I installed ResponseObserver in HttpClient, but it only goes there if http status is 200.

I also tried to use HttpResponseValidator, but it never goes there.

Here's the code:

class ApiImpl : Api {

    private val httpClient = HttpClient {

        install(JsonFeature) {

            val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }

            serializer = KotlinxSerializer(json)
        }

        install(ResponseObserver) {

            onResponse { response ->

                //*** We get here only if http status code is 200 ***/

                println("HTTP status: ${response.status.value}")
            }
        }

        HttpResponseValidator {

            validateResponse { response: HttpResponse ->

                val statusCode = response.status.value

                //*** We never get here ***/

                println("HTTP status: $statusCode")

                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
            }
        }
    }

    override suspend fun getUsers(): List<User> {

        try {

            return httpClient.get("some url....")

        } catch (e: Exception) {

            //*** We get here in case of 404 (for example), but I don't want to repeat it in every request ***/

            println(e.message)
        }

        return emptyList()
    }
}

Solution

  • I've tested your scenario with this sample class (got and modified from official ktor github rep) and I can hit all breakpoints where you'd like to get error codes (e.g. 500)

    package com.example.kmmtest001.shared
    
    import io.ktor.client.*
    import io.ktor.client.features.*
    import io.ktor.client.features.json.*
    import io.ktor.client.features.json.serializer.*
    import io.ktor.client.features.observer.*
    import io.ktor.client.request.*
    import io.ktor.client.statement.*
    import io.ktor.http.*
    import kotlinx.coroutines.*
    
    internal expect val ApplicationDispatcher: CoroutineDispatcher
    
    class ApplicationApi {
        private val client = HttpClient {
            install(JsonFeature) {
                val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
                serializer = KotlinxSerializer(json)
            }
    
            install(ResponseObserver) {
                onResponse { response ->
                    println("HTTP status: ${response.status.value}")
                }
            }
    
            HttpResponseValidator {
                validateResponse { response: HttpResponse ->
                    val statusCode = response.status.value
    
                    println("HTTP status: $statusCode")
    
                    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
                }
            }
        }
    
        var address = Url("https://tools.ietf.org/rfc/rfc1866.txt")
    
        fun about(callback: (String) -> Unit) {
            GlobalScope.apply {
                launch(ApplicationDispatcher) {
                    val result: String = client.get {
                        url(this@ApplicationApi.address.toString())
                    }
    
                    callback(result)
                }
            }
        }
    
        fun get500ServerError(callback: (String) -> Unit) {
            GlobalScope.apply {
                launch(ApplicationDispatcher) {
                    try {
                        val result: String = client.get {
                            url("https://www.versionestabile.it/_sw/so/63812313/index500.php")
                        }
    
                        callback(result)
                    } catch (se: ServerResponseException) {
                        val statusCode = se.response?.status?.value
                        callback("HTTP status code: $statusCode --> ${se.message}")
                    } catch (ce: ClientRequestException) {
                        val statusCode = ce.response?.status?.value
                        callback("HTTP status code: $statusCode --> ${ce.message}")
                    }
                }
            }
        }
    }
    

    enter image description here

    enter image description here

    I've also tested 401 and 404 errors.

    enter image description here

    enter image description here