ktorio / ktor

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

Client silently ignoring http method #851

Closed sellmair closed 5 years ago

sellmair commented 5 years ago

Ktor Version

1.1.1

Ktor Engine Used(client or server and name)

Netty, Client

JVM Version, Operating System and Relevant Context

Android, Multiplatform

Feedback

Declaration of client

HttpClient {
            install(JsonFeature) {
                serializer = KotlinxSerializer()
            }

            defaultRequest {
                accept(ContentType.Application.Json)
                contentType(ContentType.Application.Json)
                url {
                    port = instance(Http.Tags.PORT)
                    host = instance(Http.Tags.HOST)
                    protocol = URLProtocol.HTTP
                }
            }
        }

Declaration of request:

     val call = client.call {
            method = HttpMethod.Get
            url { path("registration", "available", handle.value) }
        }

Results in

2019-01-06 18:04:23.959 10001-10001/io.sellmair.link E/AndroidRuntime: FATAL EXCEPTION: main
    Process: io.sellmair.link, PID: 10001
    kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class io.ktor.client.utils.EmptyContent. For generic classes, such as lists, please provide serializer explicitly.
        at kotlinx.serialization.PlatformUtilsKt.serializer(PlatformUtils.kt:28)
        at io.ktor.client.features.json.serializer.KotlinxSerializer.lookupSerializerByData(KotlinxSerializer.kt:91)
        at io.ktor.client.features.json.serializer.KotlinxSerializer.write(KotlinxSerializer.kt:67)
        at io.ktor.client.features.json.JsonFeature$Feature$install$1.invokeSuspend(JsonFeature.kt:56)
        at io.ktor.client.features.json.JsonFeature$Feature$install$1.invoke(Unknown Source:12)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:278)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(PipelineContext.kt:63)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:137)
        at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:157)
        at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:24)
        at io.ktor.client.HttpClient.execute(HttpClient.kt:151)
        at io.ktor.client.call.HttpClientCallKt.call(HttpClientCall.kt:80)
        at io.sellmair.link.backend.registration.internal.ApiRegistrationService.isHandleAvailable(ApiRegistrationService.kt:27)
        at io.sellmair.link.backend.registration.internal.OwnerRegistrationFunctions$setHandle$1.invokeSuspend(OwnerRegistrationFunctions.kt:51)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Declaration of request with body

       val call = client.call {
            method = HttpMethod.Get
            body = ""
            url { path("registration", "available", handle.value) }
        }

Still sends a post request to the server. Ktor server side logging:

18:06:44.554 [nettyCallPool-4-2] DEBUG ktor.application - Unhandled: POST - /registration/available/b
e5l commented 5 years ago

Hi @sellmair, it looks like the side effect from HttpUrlConnection: the request type automatically changes to post depends on body configuration.

Could you reproduce it with OkHttp engine?

wollnyst commented 5 years ago

I noticed a similar effect which could explain the Exception, but the post change:

The JsonFeature only takes into account the Content-Type of the request, whether it should serialize the payload or not. If the payload is already instance of OutgoingContent (like EmptyContent) this does not make any sense and will fail with the exception above. (see here)

I think it's totally valid to have another guard to check the payload instance here. Something like:

if (payload is OutgoingContent) {
  return@intercept
}

Not sure if this solves your problem, I have written the following workaround

class IgnoreOutgoingContentJsonSerializer(private val delegate: JsonSerializer) : JsonSerializer by delegate {
    override fun write(data: Any): OutgoingContent {
        if (data is OutgoingContent) {
            return data
        }
        return delegate.write(data)
    }
}

fun JsonSerializer.ignoreOutgoingContent() = IgnoreOutgoingContentJsonSerializer(this)

val client = HttpClient(OkHttp) {
    install(JsonFeature) {
        serializer = KotlinxSerializer().ignoreOutgoingContent()
    }
}