Handle Api response in Ktor

2.9k views Asked by At

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()
    }
}
1

There are 1 answers

2
Aleksei Tirman On BEST ANSWER

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