在 Ktor 中处理 Api 响应

Handle Api response in Ktor

嘿,我正在努力为我的 api 请求学习 ktor。我正在阅读 Response validation 文档。

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

我有这个 ApiResponse class,我想通过这个 post 处理 api 响应。正如您在 link 中看到的,有一个名为 fetchResult 的函数,里面的代码每次都会检查 response.code() 并根据特定域进行路由 Success错误 正文。有没有更好的方法让它自动在特定域上路由而不是每次都在 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()
    }
}

您可以通过在 Transform 阶段拦截管道来推送 HttpResponsePipeline 类型的对象 ApiResponse 。在拦截器中,您可以使用 ContentConverter 将响应主体反序列化为泛型对象 (out T : Any)。请注意,此解决方案不允许您捕获网络异常。如果你想处理网络或其他异常,你必须编写一个函数,它将 return 一个 ApiResponse 类型的对象,如文章中所述。这是一个完整的例子:

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(), 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(): TypeInfo {
    // Error handling is needed here
    val typeProjection = kotlinType?.arguments?.get(0)
    val kType = typeProjection!!.type!!
    return TypeInfo(kType.classifier as KClass<*>, kType.platformType)
}

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