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!!)
}
嘿,我在 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!!)
}