Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ktor client post request causes error `lateinit property nextElementName has not been initialized`

Tags:

kotlin

ktor

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/

like image 276
aSemy Avatar asked Oct 21 '25 11:10

aSemy


2 Answers

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.

like image 52
Aleksei Tirman Avatar answered Oct 23 '25 06:10

Aleksei Tirman


Thanks to @aleksei-tirman for pointing out that there are two extension functions in Ktor that cause this confusion.

  • io.ktor.client.request.post
  • io.ktor.client.plugins.resources.post

These 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.

like image 24
aSemy Avatar answered Oct 23 '25 06:10

aSemy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!