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.
-
-
- reconnectionTime
- Sets the reconnection delay. If the connection to the server is lost, the
- client will wait for the specified time before attempting to reconnect.
-
-
-
- showRetryEvents()
- 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.
+
+ deserialize
+ 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