java.lang.IllegalStateException:未找到请求转换:ktor 中的 XYZ

java.lang.IllegalStateException: No request transformation found: XYZ in ktor

嘿,我在 ktor 工作。我遇到了奇怪的问题,我正在尝试查找问题,但无法获得正确的参考。

androidMain

AndroidHttpClient.kt

package com.example.kotlinmultiplatformsharedmodule

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.*
import io.ktor.serialization.kotlinx.*
import io.ktor.util.reflect.*
import io.ktor.utils.io.*
import kotlinx.serialization.json.Json
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass

actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = createHttpClient(config)

var converter: KotlinxSerializationConverter? = null

fun createHttpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient {
    val httpClient = HttpClient(OkHttp) {
        config(this)
        install(Logging) {
            logger = Logger.SIMPLE
            level = LogLevel.BODY
        }
        expectSuccess = false
        install(ContentNegotiation) {
            converter = KotlinxSerializationConverter(Json {
                prettyPrint = true
                ignoreUnknownKeys = true
                explicitNulls = false
            })
        }
        engine {
            config {
                retryOnConnectionFailure(true)
                connectTimeout(30, TimeUnit.SECONDS)
                readTimeout(40, TimeUnit.SECONDS)
            }
        }
        defaultRequest {
            header("Client-Version", Platform().versionCode)
        }
        install(Auth) {
            bearer {
                loadTokens {
                    BearerTokens(tokenProvider.accessToken, "")
                }
                refreshTokens {
                    val response =
                        client.post("https://vivek-modi/api/v1/session/refresh") {
                            markAsRefreshTokenRequest()
                            contentType(ContentType.Application.Json)
                            setBody(KtorSessionCommand(tokenProvider.refreshToken))
                        }
                    if (response.status == HttpStatusCode.Unauthorized) {
                        null
                    } else {
                        val ktorLoginResponse = response.body<KtorLoginResponse>()
                        ktorLoginResponse.accessToken?.let { ktorAccessToken ->
                            ktorAccessToken.accessToken?.let { accessToken ->
                                ktorAccessToken.refreshToken?.let { refreshToken ->
                                    BearerTokens(accessToken, refreshToken)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    httpClient.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))
    }
    return httpClient
}

fun TypeInfo.ofInnerClassParameter(): TypeInfo {
    val typeProjection = kotlinType?.arguments?.get(0)
    val kType = typeProjection!!.type!!
    return TypeInfo(kType.classifier as KClass<*>, kType.platformType)
}

Platform.kt

lateinit var provider: VersionAndroidProvider
lateinit var tokenProvider: AndroidToken

actual class Platform actual constructor() {
    actual val versionCode get() = provider.version
}

interface VersionAndroidProvider {
    val version: String
}

interface AndroidToken {
    val accessToken: String
    val refreshToken: String
}

commainMain

CommonHttpClient.kt

expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient

KtorCountryApi

package com.example.kotlinmultiplatformsharedmodule

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.serialization.Serializable

class KtorCountryApi(private val httpClient: HttpClient) {
    suspend fun getCountry(): ApiResponse<KtorCountriesResponse> {
        return httpClient.get {
            url("https://vivek-modi/api/v1/address/country")
        }.body()
    }
}

@Serializable
data class KtorCountriesResponse(
    val items: List<KtorCountry>? = null
)

@Serializable
data class KtorCountry(
    val id: String? = null,
    val isCurrentCountry: Boolean? = null,
    var isoAlpha2Code: String? = null,
    var name: String? = null,
    var phonePrefix: String? = null,
    val usesPerAreaShipping: Boolean? = null
)

@Serializable
data class KtorLoginResponse(
    val accessToken: KtorAccessTokenInfo? = null,
)

@Serializable
data class KtorAccessTokenInfo(
    val accessToken: String? = null,
    val refreshToken: String? = null,
    val lastRefreshDateTime: String? = null,
)

@Serializable
data class KtorSessionCommand(
    val refreshToken: String? = null,
)

错误

2022-04-27 17:19:21.633 8417-8417/com.example.app.dev E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.app.dev, PID: 8417
    java.lang.IllegalStateException: No request transformation found: KtorSessionCommand(refreshToken=abcjks)
        at io.ktor.client.request.HttpRequestBuilder.build(HttpRequest.kt:118)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install.invokeSuspend(HttpCallValidator.kt:130)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:141)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:126)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceedWith(SuspendFunctionGun.kt:91)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install.invokeSuspend(HttpCallValidator.kt:125)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install.invoke(Unknown Source:15)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install.invoke(Unknown Source:4)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install.invokeSuspend(HttpRequestLifecycle.kt:35)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install.invoke(Unknown Source:11)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install.invoke(Unknown Source:4)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
        at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
        at io.ktor.client.HttpClient.execute$ktor_client_core(HttpClient.kt:184)
        at io.ktor.client.statement.HttpStatement.executeUnsafe(HttpStatement.kt:107)
        at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:46)
        at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:61)
        at com.example.kotlinmultiplatformsharedmodule.HttpClientKt$createHttpClient$httpClient.invokeSuspend(HttpClient.kt:121)
        at com.example.kotlinmultiplatformsharedmodule.HttpClientKt$createHttpClient$httpClient.invoke(Unknown Source:8)
        at com.example.kotlinmultiplatformsharedmodule.HttpClientKt$createHttpClient$httpClient.invoke(Unknown Source:4)
        at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken.invokeSuspend(BearerAuthProvider.kt:127)
        at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken.invoke(Unknown Source:8)
        at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken.invoke(Unknown Source:2)
        at io.ktor.client.plugins.auth.providers.AuthTokenHolder.setToken$ktor_client_auth(AuthTokenHolder.kt:47)
        at io.ktor.client.plugins.auth.providers.BearerAuthProvider.refreshToken(BearerAuthProvider.kt:126)
        at io.ktor.client.plugins.auth.Auth$Plugin$install.invokeSuspend(Auth.kt:61)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:138)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:112)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:14)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation.resumeWith(SuspendFunctionGun.kt:62)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:138)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:112)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:14)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation.resumeWith(SuspendFunctionGun.kt:62)

当我在 api 调用中收到 401 时,这给了我错误。也许在 refreshToken 逻辑中需要修复一些东西,但我不确定是什么。

更新

@AlekseiTirman 建议我在 HttpCallValidator 上设置断点后,我找到了这个原因

Fail to serialize body. Content has type: class com.example.kotlinmultiplatformsharedmodule.KtorSessionCommand, but OutgoingContent expected.
If you expect serialized body, please check that you have installed the corresponding plugin(like `ContentNegotiation`) and set `Content-Type` header.

问题是您将 KotlinxSerializationConverter() 对象分配给了 top-level converter 属性 但没有注册它在 ContentNegotiation 插件中。您可以使用 register 方法为特定内容类型注册转换器:

install(ContentNegotiation) {
    converter = KotlinxSerializationConverter(Json {
        prettyPrint = true
        ignoreUnknownKeys = true
        explicitNulls = false
    })
    register(ContentType.Application.Json, converter!!)
}