New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KTOR-4323: WebSockets use custom serializer #3000
base: main
Are you sure you want to change the base?
Changes from all commits
ea27b72
fa555e3
44b1835
3f4b6be
1fff3ab
3644b23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,62 @@ | |
|
||
package io.ktor.tests.websocket | ||
|
||
import io.ktor.serialization.kotlinx.* | ||
import io.ktor.server.application.* | ||
import io.ktor.server.cio.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.websocket.* | ||
import io.ktor.util.* | ||
import io.ktor.websocket.* | ||
import kotlinx.serialization.* | ||
import kotlinx.serialization.Serializer | ||
import kotlinx.serialization.json.* | ||
import kotlin.test.* | ||
|
||
@InternalAPI | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this annotation is needed here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because I use |
||
class CIOWebSocketTest : WebSocketEngineSuite<CIOApplicationEngine, CIOApplicationEngine.Configuration>(CIO) { | ||
init { | ||
enableSsl = false | ||
enableHttp2 = false | ||
} | ||
|
||
override fun plugins(application: Application, routingConfigurer: Routing.() -> Unit) { | ||
application.install(WebSockets) { | ||
contentConverter = KotlinxWebsocketSerializationConverter(Json) | ||
} | ||
super.plugins(application, routingConfigurer) | ||
} | ||
|
||
data class Data(val s: Int) | ||
|
||
@OptIn(ExperimentalSerializationApi::class) | ||
@Serializer(forClass = Data::class) | ||
object DataSerializer | ||
|
||
@Test | ||
fun testWebSocketCustomSerializer() = runTest { | ||
createAndStartServer { | ||
webSocket("/") { | ||
val data = receiveDeserialized(DataSerializer) | ||
sendSerialized(data, DataSerializer) | ||
} | ||
} | ||
|
||
useSocket { | ||
negotiateHttpWebSocket() | ||
|
||
val data = Data(42) | ||
output.writeFrame(Frame.Text(Json.encodeToString(DataSerializer, data)), masking = false) | ||
output.flush() | ||
|
||
val frame = input.readFrame(Long.MAX_VALUE, 0) | ||
assertIs<Frame.Text>(frame) | ||
val incomingData = Json.decodeFromString(DataSerializer, frame.readText()) | ||
assertEquals(data, incomingData) | ||
|
||
output.writeFrame(Frame.Close(), false) | ||
output.flush() | ||
assertCloseFrame() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,14 @@ | ||||||
/* | ||||||
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||||||
*/ | ||||||
|
||||||
package io.ktor.serialization | ||||||
|
||||||
import io.ktor.websocket.* | ||||||
|
||||||
public interface WebSocketSessionWithContentConverter : WebSocketSession { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW: why this interface is needed, for me it's a little strange TBH ktor/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/websocket/ClientSessions.kt Line 53 in 380762b
Line 51 in 380762b
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, but the problem is the classpath. KSerializer is defined in kotlinx-serialiation, which is only added as dependency in ktor-shared/ktor-serialization-kotlinx. To create an extension function in the client web sockets it needs to resolve KSerializer. But the actual serialization lib (kotlinx.serialization) is an implementation detail, and not available in the core module (this is the same problem with One workaround: adding kotlinx.serialization core to the dependencies of client-core /server-core There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, now I see. Now I think, that may be some additional serialization plugin should be created for kotlinx.serialization specific usage, which will support all it's cool things, like explicit serializers provider, and reduced overhead of usage reflection over typeOf serialization retrieval. But not sure if it's really needed. |
||||||
/** | ||||||
* Converter for web socket session, if plugin [WebSockets] is installed | ||||||
*/ | ||||||
public val converter: WebsocketContentConverter? | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the use case of passing serializer here? can't it be registered in module or through
@UseSerializers
annotation?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not work, if you need to serialize 3rd classes without the possibility to change them. @UseSerializers requires an annotation, and the
SerializersModule
for generic classes requires @contextual annotation too, according to the docs. It is also not possible to serializeAny
with the overloads only, but it is with a custom serializer. That's the problem :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think something like this may be useful for regular requests as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, isn't it currently possible? I tried automatic content negation never before, with web sockets it is my first usage :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add it in another PR and focus this PR on Websockets only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW: have you tried to register external serializer for 3rd party class via
SerializersModule
viacontextual
call?. According to code it should just work by fetching serialiaer byKClass
Or can you at least provide an example of what you want to archive, and it fails
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I deleted my old reproducer and at the moment reproducing the case does not work. But adding all possibilities via
contextual
orpolymorphic
to the serializer module is annoying, so passing the serializer directly at usage is nicer IMHO. And the implementation is quite forward for web sockets.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that both ways are little annoying :)
But, even it's quite forward for websockets, looks reasonable to add the same to http endpoints, but there it will be not such easy.
And the main my concern about, why it's not good - calling this method will fail, if there is non kx.serialization converter.
Or f.e. if there will be some custom converter, which delegates to kotlinx.serialization converter, but uses another encoding for some types. So using
serializersModule
will work in both cases, but providing explicit serializer will work only in case of plain kx.serialization converter - such an inconsistency isn't so good in my opionion.And in this specific use case, may be it will be much easier to not use this plugin at all, and do serialization handling manually, or write a simple plugin for your use case? :)