androidkotlinktorkotlin-multiplatform-mobile

Handle Api response in Ktor


hey I am trying to learn ktor for my api request. I am reading Response validation in doc.

ApiResponse.kt

sealed class ApiResponse<out T : Any> {
    data class Success<out T : Any>(
        val data: T?
    ) : ApiResponse<T>()

    data class Error(
        val exception: Throwable? = null,
        val responseCode: Int = -1
    ) : ApiResponse<Nothing>()

    fun handleResult(onSuccess: ((responseData: T?) -> Unit)?, onError: ((error: Error) -> Unit)?) {
        when (this) {
            is Success -> {
                onSuccess?.invoke(this.data)
            }
            is Error -> {
                onError?.invoke(this)
            }
        }
    }
}

@Serializable
data class ErrorResponse(
    var errorCode: Int = 1,
    val errorMessage: String = "Something went wrong"
)

I have this ApiResponse class in which, I want to handle api response through this post. As you see in the link, there is function called fetchResult, inside that code is checking every time response.code() and route according to domain specific Success or Error body. Is there any better way it will automatically route on specific domain rather than checking every time in ktor.

actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
    config(this)
    install(Logging) {
        logger = Logger.SIMPLE
        level = LogLevel.BODY
    }
    expectSuccess = false
    HttpResponseValidator {
        handleResponseExceptionWithRequest { exception, _ ->
            val errorResponse: ErrorResponse
            when (exception) {
                is ResponseException -> {
                    errorResponse = exception.response.body()
                    errorResponse.errorCode = exception.response.status.value
                }
            }
        }
    }
}

KtorApi.kt

class KtorApi(private val httpClient: HttpClient) {
    suspend fun getAbc(): Flow<KtorResponse> {
        return httpClient.get {
            url("abc")
        }.body()
    }
}

Solution

  • You can push through the HttpResponsePipeline an object of the ApiResponse type by intercepting the pipeline in the Transform phase. In the interceptor, you can use a ContentConverter to deserialize a response body to an object of generic type (out T : Any). Please note that this solution doesn't allow you to catch network exceptions. If you want to handle network or other exceptions you have to write a function that will return an object of the ApiResponse type as described in the article. Here is a complete example:

    import io.ktor.client.*
    import io.ktor.client.call.*
    import io.ktor.client.engine.apache.*
    import io.ktor.client.request.*
    import io.ktor.client.statement.*
    import io.ktor.serialization.*
    import io.ktor.serialization.kotlinx.*
    import io.ktor.util.reflect.*
    import io.ktor.utils.io.*
    import kotlinx.coroutines.*
    import kotlinx.serialization.json.Json
    import kotlin.reflect.KClass
    
    fun main(): Unit = runBlocking {
        val client = HttpClient(Apache)
    
        val converter = KotlinxSerializationConverter(Json { ignoreUnknownKeys = true })
        client.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->
            if (body !is ByteReadChannel) return@intercept
    
            val response = context.response
            val apiResponse = if (response.status.value in 200..299) {
                ApiResponse.Success(
                    converter.deserialize(context.request.headers.suitableCharset(), info.ofInnerClassParameter(0), body)
                )
            } else {
                ApiResponse.Error(responseCode = response.status.value)
            }
    
            proceedWith(HttpResponseContainer(info, apiResponse))
        }
    
        val r: ApiResponse<HttpBin> = client.get("https://httpbin.org/get").body()
        r.handleResult({ data ->
            println(data?.origin)
        }) { error ->
            println(error.responseCode)
        }
    }
    
    fun TypeInfo.ofInnerClassParameter(index: Int): TypeInfo {
        // Error handling is needed here
        val kTypeProjection = kotlinType!!.arguments[index]
        val kType = kTypeProjection.type!!
        return TypeInfo(kType.classifier as KClass<*>, kType.platformType, kType)
    }
    
    @kotlinx.serialization.Serializable
    data class HttpBin(val origin: String)
    
    sealed class ApiResponse<out T : Any> {
        data class Success<out T : Any>(
            val data: T?
        ) : ApiResponse<T>()
    
        data class Error(
            val exception: Throwable? = null,
            val responseCode: Int = -1
        ) : ApiResponse<Nothing>()
    
        fun handleResult(onSuccess: ((responseData: T?) -> Unit)?, onError: ((error: Error) -> Unit)?) {
            when (this) {
                is Success -> {
                    onSuccess?.invoke(this.data)
                }
                is Error -> {
                    onError?.invoke(this)
                }
            }
        }
    }