Hey I am working in ktor. I am getting weird issue and I am trying to find the issue, but unable to get the proper reference.
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,
)
Error
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$1.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$1.invokeSuspend(HttpCallValidator.kt:125)
at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invoke(Unknown Source:15)
at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.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$1.invokeSuspend(HttpRequestLifecycle.kt:35)
at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invoke(Unknown Source:11)
at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.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$1$5$1$2.invokeSuspend(HttpClient.kt:121)
at com.example.kotlinmultiplatformsharedmodule.HttpClientKt$createHttpClient$httpClient$1$5$1$2.invoke(Unknown Source:8)
at com.example.kotlinmultiplatformsharedmodule.HttpClientKt$createHttpClient$httpClient$1$5$1$2.invoke(Unknown Source:4)
at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken$1.invokeSuspend(BearerAuthProvider.kt:127)
at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken$1.invoke(Unknown Source:8)
at io.ktor.client.plugins.auth.providers.BearerAuthProvider$refreshToken$newToken$1.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$2.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$1.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$1.resumeWith(SuspendFunctionGun.kt:62)
When I am getting 401 in api call then this giving me error. Maybe there is something need to fix in refreshToken logic, but I am not sure what.
UPDATE
After @AlekseiTirman suggestion I put breakpoint on HttpCallValidator, I found this cause
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.
The problem is that you assigned
KotlinxSerializationConverter()
object to the top-levelconverter
property but didn't register it in theContentNegotiation
plugin. You can register a converter for a specific content type with theregister
method: