I'm trying to use ktor to make a simple post requests to a remote API. There's a single endpoint, and the request and response body is in JSON.
I'm using
- ktor 2.0.1
- Kotlin 1.6.21
- Kotlinx Serialization 1.3.2
- Kotlin Coroutines 1.6.1
The endpoint I need to POST to is https://mods.factorio.com/api/v2/mods/releases/init_upload - details here.
I've created a client
private val client = HttpClient(CIO) {
install(Resources)
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
defaultRequest {
header(HttpHeaders.Authorization, "Bearer 123")
}
followRedirects = false
expectSuccess = true
}
and I use it to make a post request
val response = client.post(portalUploadEndpoint) {
contentType(ContentType.Application.Json)
setBody(InitUploadRequest("123"))
}
The request and response body are JSON objects, and I'm using Kotlinx Serialization. (See the full code at the bottom of this question.)
However I get an error. Am I doing something wrong?
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property nextElementName has not been initialized
at io.ktor.resources.serialization.ParametersEncoder.encodeValue(ParametersEncoder.kt:26)
at kotlinx.serialization.encoding.AbstractEncoder.encodeString(AbstractEncoder.kt:51)
at kotlinx.serialization.internal.StringSerializer.serialize(Primitives.kt:141)
at kotlinx.serialization.internal.StringSerializer.serialize(Primitives.kt:138)
at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeSerializableValue(Encoding.kt:282)
at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableValue(AbstractEncoder.kt:18)
at io.ktor.resources.serialization.ResourcesFormat.encodeToParameters(ResourcesFormat.kt:90)
at io.ktor.resources.UrlBuilderKt.href(UrlBuilder.kt:49)
at MainKt$main$1.invokeSuspend(main.kt:163)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(main.kt:47)
at MainKt.main(main.kt)
full code
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.resources.Resources
import io.ktor.client.plugins.resources.post
import io.ktor.client.request.header
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.URLBuilder
import io.ktor.http.contentType
import io.ktor.http.takeFrom
import io.ktor.resources.Resource
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
private val client = HttpClient(CIO) {
install(Resources)
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
defaultRequest {
header(HttpHeaders.Authorization, "Bearer 123")
}
followRedirects = false
expectSuccess = true
}
fun main() = runBlocking {
val apiBase = "https://mods.factorio.com/api/v2"
val uploadEndpoint = "mods/releases/"
val portalUploadEndpoint = URLBuilder(apiBase).apply {
takeFrom(uploadEndpoint)
}.buildString()
println(portalUploadEndpoint)
val response = client.post(portalUploadEndpoint) {
contentType(ContentType.Application.Json)
setBody(InitUploadRequest("123"))
}
println(response)
}
@Resource("/init_upload")
@Serializable
data class InitUploadRequest(
@SerialName("mod") val modName: String,
)
@Serializable(with = InitUploadResponse.Serializer::class)
sealed interface InitUploadResponse {
/**
* @param[uploadUrl] URL the mod zip file should be uploaded to
*/
@Serializable
data class Success(
@SerialName("upload_url") val uploadUrl: String,
) : InitUploadResponse
object Serializer :
JsonContentPolymorphicSerializer<InitUploadResponse>(InitUploadResponse::class) {
override fun selectDeserializer(element: JsonElement) = when {
"upload_url" in element.jsonObject -> Success.serializer()
else -> Failure.serializer()
}
}
}
@Serializable
data class Failure(
val error: String? = null,
val message: String? = null,
) : InitUploadResponse
See also
I've also report this on YouTrack https://youtrack.jetbrains.com/issue/KTOR-4342/