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