Ktor: "kotlin.IllegalStateException: Fail to send body. Content has type: class kotlinx.serialization.json.JsonObject, but OutgoingContent expected." when acceptContentTypes and contentType are differents

Created on 23 Mar 2020  路  9Comments  路  Source: ktorio/ktor

Ktor Version and Engine Used (client or server and name)
1.3.2, client Curl or IOS

Describe the bug
IllegalStateException is thrown when acceptContentTypes and contentType are different.
When both are same, the client call is ok and http response is correct.

To Reproduce
Steps to reproduce the behavior:

@Serializable
data class User(
    val email: String,
    val password: String
)

@UnstableDefault
@KtorExperimentalAPI
class Sample {
    val client: HttpClient by lazy {
        HttpClient(Curl.create()) {
            expectSuccess = false

            install(JsonFeature) {
                acceptContentTypes = listOf(
                    ContentType.parse("application/vnd.any.response+json")
                )
                serializer = KotlinxSerializer()
            }
        }
    }
}

@ImplicitReflectionSerializer
@KtorExperimentalAPI
@UnstableDefault
fun main() = runBlocking {
    val response: HttpResponse = Sample().client.put {
        url("https://httpbin.org/put")
        contentType(
            ContentType.parse("application/vnd.any+json")
        )
        body = Json(JsonConfiguration.Stable).toJson(User("[email protected]", "password1234"))
    }

    println(response.readText())
}
Uncaught Kotlin exception: kotlin.IllegalStateException: Fail to send body. Content has type: class kotlinx.serialization.json.JsonObject, but OutgoingContent expected.
        at 0   untitled.kexe                       0x00000001002e6317 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 87 (/Users/teamcity3/buildAgent/work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Throwable.kt:22:37)
        at 1   untitled.kexe                       0x00000001002df5d5 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 85 (/Users/teamcity3/buildAgent/work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Exceptions.kt:23:44)
        at 2   untitled.kexe                       0x00000001002df775 kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 85 (/Users/teamcity3/buildAgent/work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Exceptions.kt:34:44)
        at 3   untitled.kexe                       0x00000001002dfc35 kfun:kotlin.IllegalStateException.<init>(kotlin.String?)kotlin.IllegalStateException + 85 (/Users/teamcity3/buildAgent/work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Exceptions.kt:70:44)
        at 4   untitled.kexe                       0x00000001005d0206 kfun:io.ktor.client.features.HttpSend.Feature.$install$lambda-0COROUTINE$26.invokeSuspend#internal + 5270 (/Users/teamcity3/buildAgent/work/4d622a065c544371/backend.native/build/stdlib/kotlin/util/Preconditions.kt:98:15)
        at 5   untitled.kexe                       0x00000001005d0728 kfun:io.ktor.client.features.HttpSend.Feature.$install$lambda-0COROUTINE$26.invoke#internal + 312 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/features/HttpSend.kt:72:71)
        at 6   untitled.kexe                       0x0000000100549d22 kfun:io.ktor.util.pipeline.SuspendFunctionGun.loop#internal + 1122 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/PipelineContext.kt:207:0)
        at 7   untitled.kexe                       0x000000010054937b kfun:io.ktor.util.pipeline.SuspendFunctionGun.proceed#internal + 395 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/PipelineContext.kt:18:0)
        at 8   untitled.kexe                       0x00000001005ccdec kfun:io.ktor.client.features.HttpRequestLifecycle.Feature.$install$lambda-0COROUTINE$25.invokeSuspend#internal + 1212 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/features/HttpRequestLifecycle.kt:34:21)
        at 9   untitled.kexe                       0x00000001005cd508 kfun:io.ktor.client.features.HttpRequestLifecycle.Feature.$install$lambda-0COROUTINE$25.invoke#internal + 312 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/features/HttpRequestLifecycle.kt:28:73)
        at 10  untitled.kexe                       0x0000000100549d22 kfun:io.ktor.util.pipeline.SuspendFunctionGun.loop#internal + 1122 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/PipelineContext.kt:207:0)
        at 11  untitled.kexe                       0x000000010054937b kfun:io.ktor.util.pipeline.SuspendFunctionGun.proceed#internal + 395 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/PipelineContext.kt:18:0)
        at 12  untitled.kexe                       0x00000001005497da kfun:io.ktor.util.pipeline.SuspendFunctionGun.execute#internal + 474 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/PipelineContext.kt:183:16)
        at 13  untitled.kexe                       0x0000000100543909 kfun:io.ktor.util.pipeline.Pipeline.execute(TContext;TSubject)TSubject + 361 (/opt/buildAgent/work/a85294440dc5c6e/ktor-utils/common/src/io/ktor/util/pipeline/Pipeline.kt:27:41)
        at 14  untitled.kexe                       0x00000001005a44ed kfun:io.ktor.client.HttpClient.$executeCOROUTINE$0.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 653 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt:164:25)
        at 15  untitled.kexe                       0x00000001005a4834 kfun:io.ktor.client.HttpClient.execute(io.ktor.client.request.HttpRequestBuilder)io.ktor.client.call.HttpClientCall + 308 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt:163:13)
        at 16  untitled.kexe                       0x00000001005e0c0f kfun:io.ktor.client.statement.HttpStatement.$executeUnsafeCOROUTINE$96.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 943 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:104:27)
        at 17  untitled.kexe                       0x00000001005e0f1b kfun:io.ktor.client.statement.HttpStatement.executeUnsafe$ktor-client-core()io.ktor.client.statement.HttpResponse + 235 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:101:22)
        at 18  untitled.kexe                       0x00000001005dfbbb kfun:io.ktor.client.statement.HttpStatement.$executeCOROUTINE$93.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 1259 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:43:38)
        at 19  untitled.kexe                       0x00000001005e0584 kfun:io.ktor.client.statement.HttpStatement.execute(kotlin.coroutines.SuspendFunction1<io.ktor.client.statement.HttpResponse,T>){0<kotlin.Any?>}Generic + 308 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:42:13)
        at 20  untitled.kexe                       0x00000001005e06f4 kfun:io.ktor.client.statement.HttpStatement.execute()io.ktor.client.statement.HttpResponse + 228 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:58:43)
        at 21  untitled.kexe                       0x000000010028c905 kfun:sample.$main$lambda-0COROUTINE$0.invokeSuspend#internal + 6117 (/opt/buildAgent/work/a85294440dc5c6e/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt:71:32)
        at 22  untitled.kexe                       0x00000001003068dc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl.resumeWith(kotlin.Result<kotlin.Any?>) + 700 (/Users/teamcity3/buildAgent/work/4d622a065c544371/runtime/src/main/kotlin/kotlin/coroutines/ContinuationImpl.kt:26:0)
        at 23  untitled.kexe                       0x0000000100467d0a kfun:kotlinx.coroutines.DispatchedTask.run() + 2570 (/opt/buildAgent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt:42:0)
        at 24  untitled.kexe                       0x000000010044241d kfun:kotlinx.coroutines.EventLoopImplBase.processNextEvent()kotlin.Long + 813 (/opt/buildAgent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/common/src/EventLoop.common.kt:272:20)
        at 25  untitled.kexe                       0x00000001004748f8 kfun:kotlinx.coroutines.BlockingCoroutine.joinBlocking#internal + 1864 (/opt/buildAgent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/native/src/Builders.kt:65:44)
        at 26  untitled.kexe                       0x0000000100473aaf kfun:kotlinx.coroutines.runBlocking(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,T>){0<kotlin.Any?>}Generic + 1231 (/opt/buildAgent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/native/src/Builders.kt:50:22)
        at 27  untitled.kexe                       0x0000000100473fca kfun:kotlinx.coroutines.runBlocking$default(kotlin.coroutines.CoroutineContext?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,T>;kotlin.Int){0<kotlin.Any?>}Generic + 330 (/opt/buildAgent/work/44ec6e850d5c63f0/kotlinx-coroutines-core/native/src/Builders.kt:33:8)
        at 28  untitled.kexe                       0x000000010028aff2 kfun:sample.main() + 162 (/Users/niccheva/code/kotlin/untitled/src/macosMain/kotlin/sample/SampleMacos.kt:47:14)
        at 29  untitled.kexe                       0x000000010028e06a Konan_start + 138 (/Users/niccheva/code/kotlin/untitled/src/macosMain/kotlin/sample/SampleMacos.kt:47:1)

Expected behavior
Expected client call to works when accept and content type are different.

Client bug

All 9 comments

The same code with ktor 1.3.0 produce:
```
kotlin.ClassCastException: kotlinx.serialization.json.JsonObject cannot be cast to io.ktor.client.call.HttpClientCall
````
But also works if accept and content type are same

Hi @niccheva,

I checked your snippet and it looks like the server responses with content-type application/vnd.any+json on the request with the same content type. At the same time your code is the following:

acceptContentTypes = listOf(
    ContentType.parse("application/vnd.any.response+json")
)

If you replace it by the following:

acceptContentTypes = listOf(
    ContentType.parse("application/vnd.any+json")
)

It will work fine.

Hi @dmitrievanthony,

I took https://httpbin.org/put for example purpose. But on the real server, I must send a different content-type from the accept.

If you try this request with curl:

curl --location --request PUT 'https://httpbin.org/put' \
--header 'Content-Type: application/vnd.any+json' \
--header 'Accept: application/vnd.any.response+json' \
--data-raw '{
    "email": "[email protected]",
    "password": "password1234"
}'

It will work fine but ktor will crash.

Ktor request when content-type and accept are differents should works.

Yes, my point is that when you specify Accept: application/vnd.any.response+json the actual response type is just application/json:

curl --location -sD - --request PUT 'http://httpbin.org/put' \
--header 'Content-Type: application/vnd.any+json' \
--header 'Accept: application/vnd.any.response+json' \
--data-raw '{
    "email": "[email protected]",
    "password": "password1234"
}'
HTTP/1.1 200 OK
Date: Tue, 31 Mar 2020 09:49:56 GMT
Content-Type: application/json
Content-Length: 561
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args": {},
  "data": "{\n    \"email\": \"[email protected]\",\n    \"password\": \"password1234\"\n}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "application/vnd.any.response+json",
    "Content-Length": "64",
    "Content-Type": "application/vnd.any+json",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.66.0",
    "X-Amzn-Trace-Id": "Root=1-5e831244-ddf9b96abcad81e8948129dd"
  },
  "json": {
    "email": "[email protected]",
    "password": "password1234"
  },
  "origin": "5.19.178.55",
  "url": "http://httpbin.org/put"
}

But in JsonFeature you specified that this feature works only with application/vnd.any+json, not with application/json:

install(JsonFeature) {
    acceptContentTypes = listOf(
        ContentType.parse("application/vnd.any.response+json")
    )
    serializer = KotlinxSerializer()
}

So that, response with type application/json is not handled by JsonFeature.

To fix this problem you just need to specify in JsonFeature actual content types of response.

The real server respond with the content-type that is specified in the accept header. (Sorry I can't let you test with the real server...)

But with this example, the exception is thrown before the request is send, it seems ktor crash in serialization of the request body.

Well, that's the truth. I confused about everything. I spent some time investigating the JsonFeature code and realized that acceptedContentTypes there should contain all content types (received and sent) that should be serialized.

In your example, you mention in the feature that it should work only with application/vnd.any.response+json, but at the same time you send application/vnd.any+json and assume it should be serialized also.

I would recommend you to fix it the following way:

            install(JsonFeature) {
                acceptContentTypes = listOf(
                    ContentType.parse("application/vnd.any.response+json"),
                    ContentType.parse("application/vnd.any+json")
                )
                serializer = KotlinxSerializer()
            }

Hi @niccheva,

Has the last approach helped you to fix the issue?

Hi @dmitrievanthony,

Thanks for your response.
Your last approach helped me to fix the issue, it works but I have another problem here.
As you can see here, "Accept": "application/vnd.any.response+json;application/vnd.any+json", the acceptContentTypes send all the list to the Accept header.
In my case, my real server send me a 406 Not acceptable if the Accept header is not valid.
Do you have a way to send only one Accept header to the server but keep the list of content type in JsonFeature ?
I know my case is specific, but I can't do it otherwise

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shinriyo picture shinriyo  路  4Comments

PatrickLemke picture PatrickLemke  路  3Comments

seanf picture seanf  路  3Comments

KennethanCeyer picture KennethanCeyer  路  4Comments

SimonSchubert picture SimonSchubert  路  4Comments