ktorio / ktor

Framework for quickly creating connected applications in Kotlin with minimal effort
https://ktor.io
Apache License 2.0
12.78k stars 1.04k forks source link

Ktor Jackson: when sending empty content and using receive with type an exception occurs #711

Open haneev opened 5 years ago

haneev commented 5 years ago

I found an error in ktor feature using jackson. It does not handle empty body content very well.

I have this example application:

data class Han(val x: Int)

fun main(args: Array<String>) {
    val server = embeddedServer(Netty, port = 8080) {
        install(ContentNegotiation) {
            jackson {
                propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE
            }
        }

        routing {
            post("/demo") {
                val t = call.receive(Han::class)
            }
        }
    }
    server.start(wait = true)
}

When i send with postman an empty body with contentType: application/json to /demo it crashes with the following exception:

com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
 at [Source: (InputStreamReader); line: 1, column: 0]
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4133)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3988)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3037)
    at io.ktor.jackson.JacksonConverter.convertForReceive(JacksonConverter.kt:40)
    at io.ktor.features.ContentNegotiation$Feature$install$3.doResume(ContentNegotiation.kt:95)
    at io.ktor.features.ContentNegotiation$Feature$install$3.invoke(ContentNegotiation.kt)
    at io.ktor.features.ContentNegotiation$Feature$instal...

I think this should be handled more gracefully than this hard exception.

Thank you guys for all your effort! Ktor is awesome.

Versions:

implementation("io.ktor:ktor-server-netty:0.9.5")
    implementation("io.ktor:ktor-server-core:0.9.5")
    implementation("io.ktor:ktor-jackson:0.9.5")

with kotlin 1.2.71

cy6erGn0m commented 5 years ago

Well, the error message No content to map due to end-of-input looks clear enough, isn't it? Do you expect more descriptive error or something?

haneev commented 5 years ago

Thanks for you answer :) I get your point. However, i don't like the fact that some ContentNegotiation implementation leaks their errors into ktor. How would you propose to gracefully handle this exception in this usecase? Maybe i am missing some ktor paradigm.

cy6erGn0m commented 5 years ago

We can't handle all possible serialization/deserialization errors anyway and there is no reasons to wrap/rename original errors (it will cause code duplication to detect errors + a deserializer always have more information to report exact error). So this is why by-passing them is the best I'd say. Also note that not all particular cases could be robustly detected at ktor side. For example, an empty body could be detected only if there is Content-Length: 0 header because of streaming.

RiccardoM commented 5 years ago

In order to solve this, you could simply create an extension functions such as

/**
 * Receives content for this request.
 * @return instance of [T] received from this call, or `null` if content cannot be transformed to the requested type.
 */
suspend inline fun <reified T : Any> ApplicationCall.receiveJsonOrNull(): T? = receiveJsonOrNull(T::class)

/**
 * Receives content for this request.
 * @param type instance of `KClass` specifying type to be received.
 * @return instance of [T] received from this call, or `null` if content cannot be transformed to the requested type..
 */
suspend fun <T : Any> ApplicationCall.receiveJsonOrNull(type: KClass<T>): T? {
    return try {
        receive(type)
    } catch (cause: Throwable) {
        application.log.debug("Conversion failed, null returned", cause)
        null
    }
}

And later use it as follows:

val value = call.receiveJsonOrNull<MyClass>()
camhashemi commented 5 years ago

@cy6erGn0m the javadoc for receiveOrNull says:

@return instance of [T] received from this call, or `null` if content cannot be transformed to the requested type.

doesn't the above exception include the null case? this is what I expect. I've run into this issue as well.

thanks to @RiccardoM for the solution, perhaps it can be merged?

oleg-larshin commented 4 years ago

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