diff --git a/ktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ConverterTest.kt b/ktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ConverterTest.kt new file mode 100644 index 00000000000..90e3bc38b2b --- /dev/null +++ b/ktor-shared/ktor-serialization/ktor-serialization-gson/jvm/test/ConverterTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import io.ktor.serialization.gson.GsonConverter +import io.ktor.util.reflect.typeInfo +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.charsets.Charsets +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.time.Duration.Companion.milliseconds + +@Serializable +data class Payload(val value: String) + +class ConverterTest { + @Test + fun returnsNullForEmptyChannelWithDelayedClose() = runTest { + val channel = ByteChannel() + launch { + delay(200.milliseconds) + channel.close() + } + + val converter = GsonConverter() + val result = converter.deserialize(Charsets.UTF_8, typeInfo(), channel) + assertNull(result) + } +} diff --git a/ktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ConverterTest.kt b/ktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ConverterTest.kt new file mode 100644 index 00000000000..7d7ca0e080d --- /dev/null +++ b/ktor-shared/ktor-serialization/ktor-serialization-jackson/jvm/test/ConverterTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import com.fasterxml.jackson.databind.exc.MismatchedInputException +import io.ktor.serialization.JsonConvertException +import io.ktor.serialization.jackson.JacksonConverter +import io.ktor.util.reflect.typeInfo +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.charsets.Charsets +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.time.Duration.Companion.milliseconds + +@Serializable +data class Payload(val value: String) + +class ConverterTest { + @Test + fun throwsExceptionForEmptyChannel() = runTest { + val channel = ByteChannel() + launch { + delay(200.milliseconds) + channel.close() + } + + val converter = JacksonConverter() + val cause = assertFailsWith { + converter.deserialize(Charsets.UTF_8, typeInfo(), channel) + } + + assertIs(cause.cause) + } +} diff --git a/ktor-shared/ktor-serialization/ktor-serialization-jackson3/jvm/test/ConverterTest.kt b/ktor-shared/ktor-serialization/ktor-serialization-jackson3/jvm/test/ConverterTest.kt new file mode 100644 index 00000000000..38862ddea98 --- /dev/null +++ b/ktor-shared/ktor-serialization/ktor-serialization-jackson3/jvm/test/ConverterTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import io.ktor.serialization.JsonConvertException +import io.ktor.serialization.jackson3.JacksonConverter +import io.ktor.util.reflect.typeInfo +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.charsets.Charsets +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import tools.jackson.databind.exc.MismatchedInputException +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.time.Duration.Companion.milliseconds + +@Serializable +data class Payload(val value: String) + +class ConverterTest { + @Test + fun throwsExceptionForEmptyChannel() = runTest { + val channel = ByteChannel() + launch { + delay(200.milliseconds) + channel.close() + } + + val converter = JacksonConverter() + val cause = assertFailsWith { + converter.deserialize(Charsets.UTF_8, typeInfo(), channel) + } + + assertIs(cause.cause) + } +} diff --git a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/build.gradle.kts b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/build.gradle.kts index c320ee21e2a..8e4b343c2d7 100644 --- a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/build.gradle.kts +++ b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/build.gradle.kts @@ -15,5 +15,9 @@ kotlin { api(projects.ktorSerialization) api(libs.kotlinx.serialization.core) } + commonTest.dependencies { + implementation(projects.ktorSerializationKotlinxJson) + implementation(libs.kotlinx.coroutines.test) + } } } diff --git a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationConverter.kt b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationConverter.kt index 456ad59cc61..70621bdf477 100644 --- a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationConverter.kt +++ b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/src/io/ktor/serialization/kotlinx/KotlinxSerializationConverter.kt @@ -7,8 +7,6 @@ package io.ktor.serialization.kotlinx import io.ktor.http.* import io.ktor.http.content.* import io.ktor.serialization.* -import io.ktor.util.* -import io.ktor.util.pipeline.* import io.ktor.util.reflect.* import io.ktor.utils.io.* import io.ktor.utils.io.charsets.* @@ -16,7 +14,6 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.flow.* import kotlinx.io.* import kotlinx.serialization.* -import kotlin.jvm.* /** * Creates a converter serializing with the specified string [format] @@ -58,13 +55,14 @@ public class KotlinxSerializationConverter( } override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { - val fromExtension = extensions.asFlow() - .map { it.deserialize(charset, typeInfo, content) } - .firstOrNull { it != null || content.isClosedForRead } - if (extensions.isNotEmpty() && (fromExtension != null || content.isClosedForRead)) return fromExtension + val contentPacket = content.readRemaining() + + for (ext in extensions) { + if (contentPacket.exhausted()) return null + return ext.deserialize(charset, typeInfo, ByteReadChannel(contentPacket)) ?: continue + } val serializer = format.serializersModule.serializerForTypeInfo(typeInfo) - val contentPacket = content.readRemaining() try { return when (format) { diff --git a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/test/io/ktor/serialization/kotlinx/ConverterTest.kt b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/test/io/ktor/serialization/kotlinx/ConverterTest.kt new file mode 100644 index 00000000000..30102fdcf73 --- /dev/null +++ b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/common/test/io/ktor/serialization/kotlinx/ConverterTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2026 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.serialization.kotlinx + +import io.ktor.serialization.kotlinx.json.DefaultJson +import io.ktor.util.reflect.typeInfo +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.charsets.Charsets +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.time.Duration.Companion.milliseconds + +@Serializable +data class Payload(val value: String) + +class ConverterTest { + @Test + fun returnsNullForEmptyChannelWithDelayedClose() = runTest { + val channel = ByteChannel() + launch { + delay(200.milliseconds) + channel.close() + } + + val converter = KotlinxSerializationConverter(DefaultJson) + val result = converter.deserialize(Charsets.UTF_8, typeInfo(), channel) + assertNull(result) + } +}