From 838070ca283d4a0481703ebc7fed43331c677cba Mon Sep 17 00:00:00 2001 From: Viktoriya Nikolova Date: Mon, 14 Apr 2025 18:25:00 +0300 Subject: [PATCH 1/4] KTOR-8181 Document client SSE reconnection --- topics/client-server-sent-events.topic | 52 +++++++++++++++----------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/topics/client-server-sent-events.topic b/topics/client-server-sent-events.topic index 3e657a28a..0788630b9 100644 --- a/topics/client-server-sent-events.topic +++ b/topics/client-server-sent-events.topic @@ -47,29 +47,37 @@ You can optionally configure the SSE plugin within the install block by setting the supported properties of the SSEConfig - class: + class.

- - - <code>reconnectionTime</code> - Sets the reconnection delay. If the connection to the server is lost, the - client will wait for the specified time before attempting to reconnect. - - - <code>showCommentEvents()</code> - Adds events that contain only comments in the incoming flow. - - - <code>showRetryEvents()</code> - Adds events that contain only the retry field in the incoming flow. - - -

- In the following example, the SSE plugin is installed into the HTTP client and configured to include events - that only contain comments and those that contain only the retry field in the incoming flow: -

- + + +

Limitations: not supported with the OkHttp engine.

+
+

+ To enable automatic reconnection with supported engines, set + maxReconnectionAttempts to a value greater than 0. You can also configure the + delay between attempts using reconnectionTime: +

+ + install(SSE) { + maxReconnectionAttempts = 4 + reconnectionTime = 2.seconds + } + +

+ If the connection to the server is lost, the client will wait for the specified + reconnectionTime before attempting to reconnect. It will make up to + the specified number of maxReconnectionAttempts attempts to reestablish the connection. +

+
+ +

+ In the following example, the SSE plugin is installed into the HTTP client and configured to include events + that only contain comments and those that contain only the retry field in the incoming flow: +

+ +

From 393ec5f3183597b60dbc36474c862a3d079bfa1c Mon Sep 17 00:00:00 2001 From: Viktoriya Nikolova Date: Sun, 20 Apr 2025 08:00:32 +0300 Subject: [PATCH 2/4] KTOR-7776 Add documentation and examples for client and server SSE serialization --- .../snippets/client-sse/build.gradle.kts | 2 + .../main/kotlin/com.example/Application.kt | 33 ++++++++++++- .../snippets/server-sse/build.gradle.kts | 2 + .../main/kotlin/com/example/Application.kt | 18 +++++++ .../kotlin/com/example/ApplicationTest.kt | 24 +++++++++- topics/client-server-sent-events.topic | 47 ++++++++++++++----- topics/server-server-sent-events.topic | 25 +++++++++- 7 files changed, 136 insertions(+), 15 deletions(-) diff --git a/codeSnippets/snippets/client-sse/build.gradle.kts b/codeSnippets/snippets/client-sse/build.gradle.kts index fd4a430a5..2ec454114 100644 --- a/codeSnippets/snippets/client-sse/build.gradle.kts +++ b/codeSnippets/snippets/client-sse/build.gradle.kts @@ -6,6 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") + kotlin("plugin.serialization").version("2.1.20") } application { @@ -25,6 +26,7 @@ dependencies { implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") implementation("io.ktor:ktor-client-logging:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("junit:junit:$junit_version") testImplementation("org.hamcrest:hamcrest:$hamcrest_version") diff --git a/codeSnippets/snippets/client-sse/src/main/kotlin/com.example/Application.kt b/codeSnippets/snippets/client-sse/src/main/kotlin/com.example/Application.kt index b036709f7..c28eefcf0 100644 --- a/codeSnippets/snippets/client-sse/src/main/kotlin/com.example/Application.kt +++ b/codeSnippets/snippets/client-sse/src/main/kotlin/com.example/Application.kt @@ -2,7 +2,18 @@ package com.example import io.ktor.client.* import io.ktor.client.plugins.sse.* +import io.ktor.client.request.* +import io.ktor.sse.* import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer + +@Serializable +data class Customer(val id: Int, val firstName: String, val lastName: String) + +@Serializable +data class Product(val id: Int, val prices: List) fun main() { val client = HttpClient { @@ -20,6 +31,26 @@ fun main() { } } } + + // example with deserialization + client.sse({ + url("http://localhost:8080/serverSentEvents") + }, deserialize = { + typeInfo, jsonString -> + val serializer = Json.serializersModule.serializer(typeInfo.kotlinType!!) + Json.decodeFromString(serializer, jsonString)!! + }) { // `this` is `ClientSSESessionWithDeserialization` + incoming.collect { event: TypedServerSentEvent -> + when (event.event) { + "customer" -> { + val customer: Customer? = deserialize(event.data) + } + "product" -> { + val product: Product? = deserialize(event.data) + } + } + } + } + client.close() } - client.close() } \ No newline at end of file diff --git a/codeSnippets/snippets/server-sse/build.gradle.kts b/codeSnippets/snippets/server-sse/build.gradle.kts index f77c64696..7271f9bee 100644 --- a/codeSnippets/snippets/server-sse/build.gradle.kts +++ b/codeSnippets/snippets/server-sse/build.gradle.kts @@ -5,6 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") + kotlin("plugin.serialization").version("2.1.20") } application { @@ -21,6 +22,7 @@ dependencies { implementation("io.ktor:ktor-server-core:$ktor_version") implementation("io.ktor:ktor-server-netty:$ktor_version") implementation("io.ktor:ktor-server-sse:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test") diff --git a/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt index f1a7a40c0..3f55d5dab 100644 --- a/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt +++ b/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt @@ -5,6 +5,15 @@ import io.ktor.server.routing.* import io.ktor.server.sse.* import io.ktor.sse.* import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer + +@Serializable +data class Customer(val id: Int, val firstName: String, val lastName: String) + +@Serializable +data class Product(val id: Int, val prices: List) fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) @@ -18,5 +27,14 @@ fun Application.module() { delay(1000) } } + + // example with serialization + sse("/json", serialize = { typeInfo, it -> + val serializer = Json.serializersModule.serializer(typeInfo.kotlinType!!) + Json.encodeToString(serializer, it) + }) { + send(Customer(0, "Jet", "Brains")) + send(Product(0, listOf(100, 200))) + } } } diff --git a/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt b/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt index 9fcc8927b..72aad527e 100644 --- a/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt +++ b/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt @@ -25,4 +25,26 @@ class ApplicationTest { } } } -} \ No newline at end of file + + @Test + fun testJsonEvents() { + testApplication { + application { + module() + } + + val client = createClient { + install(SSE) + } + + client.sse("/json") { + incoming.collectIndexed { i, event -> + when (i) { + 0 -> assertEquals("""{"id":0,"firstName":"Jet","lastName":"Brains"}""", event.data) + 1 -> assertEquals("""{"id":0,"prices":[100,200]}""", event.data) + } + } + } + } + } +} diff --git a/topics/client-server-sent-events.topic b/topics/client-server-sent-events.topic index 0788630b9..836910edd 100644 --- a/topics/client-server-sent-events.topic +++ b/topics/client-server-sent-events.topic @@ -51,7 +51,7 @@

-

Limitations: not supported with the OkHttp engine.

+

️️Not supported on: OkHttp

To enable automatic reconnection with supported engines, set @@ -76,28 +76,34 @@ that only contain comments and those that contain only the retry field in the incoming flow:

+ include-lines="20-23"/>

A client's SSE session is represented by the - DefaultClientSSESession + + ClientSSESession + interface. This interface exposes the API that allows you to receive server-sent events from a server.

The HttpClient allows you to get access to an SSE session in one of the following ways:

-
  • The sse() +
  • The + + sse() + function creates the SSE session and allows you to act on it.
  • The - sseSession() + + sseSession() + function allows you to open an SSE session.
  • -

    The following parameters are available to both functions. To specify the URL endpoint, choose from two - options:

    +

    To specify the URL endpoint, you can choose from two options:

  • Use the urlString parameter to specify the whole URL as a string.
  • Use the schema, host, port, and path parameters @@ -146,8 +152,6 @@ An incoming server-sent events flow. - -

    The example below creates a new SSE session with the events endpoint, reads events through the incoming property and prints the received @@ -155,11 +159,32 @@ .

    + include-lines="18-33,55-56"/> +

    For the full example, see + client-sse. +

    +
    + +

    + The SSE plugin supports deserialization of server-sent events into type-safe Kotlin objects. This + feature is particularly useful when working with structured data from the server. +

    +

    + To enable deserialization, provide a custom deserialization function using the deserialize + parameter on an SSE access function and use the + + ClientSSESessionWithDeserialization + + class to handle the deserialized events. +

    +

    + Here's an example using kotlinx.serialization to deserialize JSON data: +

    +

    For the full example, see client-sse.

    - \ No newline at end of file diff --git a/topics/server-server-sent-events.topic b/topics/server-server-sent-events.topic index 80435092c..3367f2e54 100644 --- a/topics/server-server-sent-events.topic +++ b/topics/server-server-sent-events.topic @@ -91,7 +91,9 @@

    Within the sse block, you define the handler for the specified path, represented by the - ServerSSESession + + ServerSSESession + class. The following functions and properties are available within the block:

    @@ -121,7 +123,26 @@

    + include-lines="23-29,40"/> +

    For the full example, see + server-sse. +

    +
    + +

    + To enable serialization, provide a custom serialization function using the serialize + parameter on an SSE route. Inside the handler, you can use the + + ServerSSESessionWithSerialization + + class to send serialized events: +

    + +

    + The serialize function is responsible for converting data objects into JSON, which is then + placed in the data field of a ServerSentEvent. +

    For the full example, see server-sse.

    From 2f1b36cf3819e6a23109c8f6f5caa381d8feaafe Mon Sep 17 00:00:00 2001 From: Viktoriya Nikolova Date: Sun, 20 Apr 2025 08:39:33 +0300 Subject: [PATCH 3/4] KTOR-7954 Add documentation and example for SSE heartbeat --- .../main/kotlin/com/example/Application.kt | 14 +++++++++ .../kotlin/com/example/ApplicationTest.kt | 31 +++++++++++++++++++ topics/server-server-sent-events.topic | 24 ++++++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt index 3f55d5dab..f8e0ad4d4 100644 --- a/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt +++ b/codeSnippets/snippets/server-sse/src/main/kotlin/com/example/Application.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.milliseconds @Serializable data class Customer(val id: Int, val firstName: String, val lastName: String) @@ -36,5 +37,18 @@ fun Application.module() { send(Customer(0, "Jet", "Brains")) send(Product(0, listOf(100, 200))) } + + // example with heartbeat + sse("/heartbeat") { + heartbeat { + period = 10.milliseconds + event = ServerSentEvent("heartbeat") + } + // ... + repeat(4) { + send(ServerSentEvent("Hello")) + delay(10.milliseconds) + } + } } } diff --git a/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt b/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt index 72aad527e..cabb98042 100644 --- a/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt +++ b/codeSnippets/snippets/server-sse/src/test/kotlin/com/example/ApplicationTest.kt @@ -2,7 +2,9 @@ package com.example import io.ktor.client.plugins.sse.* import io.ktor.server.testing.* +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collectIndexed +import kotlinx.coroutines.withTimeout import kotlin.test.Test import kotlin.test.assertEquals @@ -47,4 +49,33 @@ class ApplicationTest { } } } + + @Test + fun testHeartbeat() { + testApplication { + application { + module() + } + + val client = createClient { + install(SSE) + } + + var hellos = 0 + var heartbeats = 0 + withTimeout(5_000) { + client.sse("/heartbeat") { + incoming.collect { event -> + when (event.data) { + "Hello" -> hellos++ + "heartbeat" -> heartbeats++ + } + if (hellos > 3 && heartbeats > 3) { + cancel() + } + } + } + } + } + } } diff --git a/topics/server-server-sent-events.topic b/topics/server-server-sent-events.topic index 3367f2e54..68095ff6f 100644 --- a/topics/server-server-sent-events.topic +++ b/topics/server-server-sent-events.topic @@ -123,11 +123,31 @@

    + include-lines="23-30,53"/>

    For the full example, see server-sse.

    + +

    + Heartbeats ensure the SSE connection stays active during periods of inactivity by periodically sending + events. As long as the session remains active, the server will send the specified event at the + configured interval. +

    +

    + To enable and configure a heartbeat, use the + + .heartbeat() + + function within an SSE route handler: +

    + +

    + In this example, a heartbeat event is sent every 10 milliseconds to maintain the + connection. +

    +

    To enable serialization, provide a custom serialization function using the serialize @@ -138,7 +158,7 @@ class to send serialized events:

    + include-lines="12-17,20-24,32-39,53-54"/>

    The serialize function is responsible for converting data objects into JSON, which is then placed in the data field of a ServerSentEvent. From 7ffc3842a1963d5d5ed3208d9168e9c1cd559983 Mon Sep 17 00:00:00 2001 From: Viktoriya Nikolova Date: Thu, 24 Apr 2025 12:38:09 +0200 Subject: [PATCH 4/4] address comments --- topics/client-server-sent-events.topic | 10 ++++++++-- topics/server-server-sent-events.topic | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/topics/client-server-sent-events.topic b/topics/client-server-sent-events.topic index 836910edd..1d7695972 100644 --- a/topics/client-server-sent-events.topic +++ b/topics/client-server-sent-events.topic @@ -51,7 +51,7 @@

    -

    ️️Not supported on: OkHttp

    +

    ️️Not supported in: OkHttp

    To enable automatic reconnection with supported engines, set @@ -67,7 +67,7 @@

    If the connection to the server is lost, the client will wait for the specified reconnectionTime before attempting to reconnect. It will make up to - the specified number of maxReconnectionAttempts attempts to reestablish the connection. + the specified maxReconnectionAttempts to reestablish the connection.

    @@ -134,6 +134,12 @@ Specifies whether to show events that contain only the retry field in the incoming flow. + + <code>deserialize</code> + A deserializer function to transform the data field of the + TypedServerSentEvent into an object. For more information, see + . + diff --git a/topics/server-server-sent-events.topic b/topics/server-server-sent-events.topic index 68095ff6f..30766074f 100644 --- a/topics/server-server-sent-events.topic +++ b/topics/server-server-sent-events.topic @@ -160,7 +160,7 @@

    - The serialize function is responsible for converting data objects into JSON, which is then + The serialize function in this example is responsible for converting data objects into JSON, which is then placed in the data field of a ServerSentEvent.

    For the full example, see