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
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)
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
I've also report this on YouTrack https://youtrack.jetbrains.com/issue/KTOR-4342/
The problem is that you're passing a String (URL) to the HttpClient.post extension function that expects a Resource. You need to pass a resource there or use the HttpClient.post extension function from the io.ktor.client.request package.
Thanks to @aleksei-tirman for pointing out that there are two extension functions in Ktor that cause this confusion.
io.ktor.client.request.postio.ktor.client.plugins.resources.postThese can clash - so make sure you use the correct one.
There was an additional problem that I've also resolved. I originally said
The request and response body are JSON objects
The requests are actually form parameters.
Instead of
val response = client.post(portalUploadEndpoint) {
contentType(ContentType.Application.Json)
setBody(InitUploadRequest("123"))
}
I changed it to
val response = client.submitForm(
url = portalUploadEndpoint,
formParameters = Parameters.build {
append("mod", modName)
}
)
The response is a JSON body, which I manually decode
val initUploadResponse: InitUploadResponse =
Json.decodeFromString(InitUploadResponse.serializer(), response.bodyAsText())
I then removed the Resources Ktor plugin from the HttpClient - it's not needed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With