diff --git a/codeSnippets/gradle.properties b/codeSnippets/gradle.properties index 2e796457a..a11af9dc0 100644 --- a/codeSnippets/gradle.properties +++ b/codeSnippets/gradle.properties @@ -5,8 +5,8 @@ kotlin.native.binary.memoryModel = experimental # gradle configuration org.gradle.configureondemand = false # versions -kotlin_version = 2.1.20 -ktor_version = 3.2.3 +kotlin_version = 2.2.10 +ktor_version = 3.3.0 kotlinx_coroutines_version = 1.10.1 kotlinx_serialization_version = 1.8.0 kotlin_css_version = 1.0.0-pre.721 diff --git a/codeSnippets/gradle/wrapper/gradle-wrapper.properties b/codeSnippets/gradle/wrapper/gradle-wrapper.properties index 707e499ac..2a84e188b 100644 --- a/codeSnippets/gradle/wrapper/gradle-wrapper.properties +++ b/codeSnippets/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/codeSnippets/settings.gradle.kts b/codeSnippets/settings.gradle.kts index 4fcf24671..e0d3fe262 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -22,8 +22,8 @@ fun module(group: String, name: String) { // --------------------------- -module("snippets", "jetty-war") -module("snippets", "tomcat-war") +//module("snippets", "jetty-war") +//module("snippets", "tomcat-war") module("snippets", "auth-basic") module("snippets", "auth-bearer") module("snippets", "auth-basic-hash-table") @@ -135,7 +135,7 @@ module("snippets", "status-pages") module("snippets", "client-download-file-range") module("snippets", "shutdown-url") module("snippets", "double-receive") -module("snippets", "tomcat-war-ssl") +//module("snippets", "tomcat-war-ssl") module("snippets", "sockets-client") module("snippets", "sockets-client-tls") module("snippets", "sockets-server") diff --git a/codeSnippets/snippets/auth-form-session-nested/build.gradle.kts b/codeSnippets/snippets/auth-form-session-nested/build.gradle.kts index a876b1f48..e9ecd7eea 100644 --- a/codeSnippets/snippets/auth-form-session-nested/build.gradle.kts +++ b/codeSnippets/snippets/auth-form-session-nested/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/auth-form-session/build.gradle.kts b/codeSnippets/snippets/auth-form-session/build.gradle.kts index 4f5d72017..3f64f1dc1 100644 --- a/codeSnippets/snippets/auth-form-session/build.gradle.kts +++ b/codeSnippets/snippets/auth-form-session/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/auth-jwt-hs256/build.gradle.kts b/codeSnippets/snippets/auth-jwt-hs256/build.gradle.kts index e12b78276..2e298bf24 100644 --- a/codeSnippets/snippets/auth-jwt-hs256/build.gradle.kts +++ b/codeSnippets/snippets/auth-jwt-hs256/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/auth-jwt-rs256/build.gradle.kts b/codeSnippets/snippets/auth-jwt-rs256/build.gradle.kts index eadbd951e..ff08cb0aa 100644 --- a/codeSnippets/snippets/auth-jwt-rs256/build.gradle.kts +++ b/codeSnippets/snippets/auth-jwt-rs256/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/auth-oauth-google/build.gradle.kts b/codeSnippets/snippets/auth-oauth-google/build.gradle.kts index a4f6ad811..35d01d384 100644 --- a/codeSnippets/snippets/auth-oauth-google/build.gradle.kts +++ b/codeSnippets/snippets/auth-oauth-google/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts b/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts index 92d418702..83d68cf17 100644 --- a/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts +++ b/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts @@ -3,7 +3,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/client-auth-oauth-google/build.gradle.kts b/codeSnippets/snippets/client-auth-oauth-google/build.gradle.kts index f475e84b7..16433f959 100644 --- a/codeSnippets/snippets/client-auth-oauth-google/build.gradle.kts +++ b/codeSnippets/snippets/client-auth-oauth-google/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-engine-js/build.gradle.kts b/codeSnippets/snippets/client-engine-js/build.gradle.kts index 0bf86a8a9..ee9962e04 100644 --- a/codeSnippets/snippets/client-engine-js/build.gradle.kts +++ b/codeSnippets/snippets/client-engine-js/build.gradle.kts @@ -3,7 +3,7 @@ val kotlinx_html_version: String by project plugins { kotlin("multiplatform") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } repositories { diff --git a/codeSnippets/snippets/client-json-kotlinx/build.gradle.kts b/codeSnippets/snippets/client-json-kotlinx/build.gradle.kts index e67a35c9d..70c6cdd3d 100644 --- a/codeSnippets/snippets/client-json-kotlinx/build.gradle.kts +++ b/codeSnippets/snippets/client-json-kotlinx/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-sse/build.gradle.kts b/codeSnippets/snippets/client-sse/build.gradle.kts index 2ec454114..92cb54963 100644 --- a/codeSnippets/snippets/client-sse/build.gradle.kts +++ b/codeSnippets/snippets/client-sse/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-testing-mock/build.gradle.kts b/codeSnippets/snippets/client-testing-mock/build.gradle.kts index ce1b2a858..36337502d 100644 --- a/codeSnippets/snippets/client-testing-mock/build.gradle.kts +++ b/codeSnippets/snippets/client-testing-mock/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-type-safe-requests/build.gradle.kts b/codeSnippets/snippets/client-type-safe-requests/build.gradle.kts index 615501f46..37d4c3db0 100644 --- a/codeSnippets/snippets/client-type-safe-requests/build.gradle.kts +++ b/codeSnippets/snippets/client-type-safe-requests/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-validate-2xx-response/build.gradle.kts b/codeSnippets/snippets/client-validate-2xx-response/build.gradle.kts index f4e0ef65b..3db37da0f 100644 --- a/codeSnippets/snippets/client-validate-2xx-response/build.gradle.kts +++ b/codeSnippets/snippets/client-validate-2xx-response/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/client-websockets-serialization/build.gradle.kts b/codeSnippets/snippets/client-websockets-serialization/build.gradle.kts index d21a8c52b..6f4754392 100644 --- a/codeSnippets/snippets/client-websockets-serialization/build.gradle.kts +++ b/codeSnippets/snippets/client-websockets-serialization/build.gradle.kts @@ -6,7 +6,7 @@ val hamcrest_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/cors/build.gradle.kts b/codeSnippets/snippets/cors/build.gradle.kts index 33425bed7..adc65030a 100644 --- a/codeSnippets/snippets/cors/build.gradle.kts +++ b/codeSnippets/snippets/cors/build.gradle.kts @@ -6,7 +6,7 @@ val junit_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts b/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts index 764d2606b..22915526e 100644 --- a/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts +++ b/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/engine-main-custom-environment/build.gradle.kts b/codeSnippets/snippets/engine-main-custom-environment/build.gradle.kts index a9b6c230d..b535417ab 100644 --- a/codeSnippets/snippets/engine-main-custom-environment/build.gradle.kts +++ b/codeSnippets/snippets/engine-main-custom-environment/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/forwarded-header/build.gradle.kts b/codeSnippets/snippets/forwarded-header/build.gradle.kts index 4b92423cd..c4e7725db 100644 --- a/codeSnippets/snippets/forwarded-header/build.gradle.kts +++ b/codeSnippets/snippets/forwarded-header/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/full-stack-task-manager/gradle/libs.versions.toml b/codeSnippets/snippets/full-stack-task-manager/gradle/libs.versions.toml index f365ecced..50de82146 100644 --- a/codeSnippets/snippets/full-stack-task-manager/gradle/libs.versions.toml +++ b/codeSnippets/snippets/full-stack-task-manager/gradle/libs.versions.toml @@ -16,7 +16,7 @@ junit = "4.13.2" kotlin = "2.2.0" kotlinx-coroutines = "1.10.2" kotlinxSerializationJson = "1.8.1" -ktor = "3.2.3" +ktor = "3.3.0" logback = "1.5.18" [libraries] diff --git a/codeSnippets/snippets/htmx-integration/build.gradle.kts b/codeSnippets/snippets/htmx-integration/build.gradle.kts index 4d7100af5..f093d107f 100644 --- a/codeSnippets/snippets/htmx-integration/build.gradle.kts +++ b/codeSnippets/snippets/htmx-integration/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } group = "com.example" diff --git a/codeSnippets/snippets/jetty-war/build.gradle.kts b/codeSnippets/snippets/jetty-war/build.gradle.kts index 314385a73..8aa2837b6 100644 --- a/codeSnippets/snippets/jetty-war/build.gradle.kts +++ b/codeSnippets/snippets/jetty-war/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("org.gretty") version "4.0.3" + id("org.gretty") version "4.1.7" id("war") } diff --git a/codeSnippets/snippets/json-kotlinx-method-override/build.gradle.kts b/codeSnippets/snippets/json-kotlinx-method-override/build.gradle.kts index 801b838a1..1ca7a22b7 100644 --- a/codeSnippets/snippets/json-kotlinx-method-override/build.gradle.kts +++ b/codeSnippets/snippets/json-kotlinx-method-override/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts b/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts index 29cc55bf1..11cb38b73 100644 --- a/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts +++ b/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts @@ -6,7 +6,7 @@ val swagger_codegen_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/json-kotlinx/build.gradle.kts b/codeSnippets/snippets/json-kotlinx/build.gradle.kts index 9be8522d3..4eccaf3b7 100644 --- a/codeSnippets/snippets/json-kotlinx/build.gradle.kts +++ b/codeSnippets/snippets/json-kotlinx/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts b/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts index b86072ca8..fa1f6540d 100644 --- a/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts +++ b/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/migrating-express-ktor/5_send_response/build.gradle.kts b/codeSnippets/snippets/migrating-express-ktor/5_send_response/build.gradle.kts index 2d539fe6a..f889acc76 100644 --- a/codeSnippets/snippets/migrating-express-ktor/5_send_response/build.gradle.kts +++ b/codeSnippets/snippets/migrating-express-ktor/5_send_response/build.gradle.kts @@ -4,8 +4,8 @@ val logback_version: String by project plugins { application - kotlin("jvm") version "2.1.20" - kotlin("plugin.serialization").version("2.1.20") + kotlin("jvm") version "2.2.10" + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/migrating-express-ktor/7_receive_request/build.gradle.kts b/codeSnippets/snippets/migrating-express-ktor/7_receive_request/build.gradle.kts index 2a6478b90..b0a6fc20e 100644 --- a/codeSnippets/snippets/migrating-express-ktor/7_receive_request/build.gradle.kts +++ b/codeSnippets/snippets/migrating-express-ktor/7_receive_request/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/migrating-express-ktor/gradle.properties b/codeSnippets/snippets/migrating-express-ktor/gradle.properties index 6645cb0d6..2d93d8298 100644 --- a/codeSnippets/snippets/migrating-express-ktor/gradle.properties +++ b/codeSnippets/snippets/migrating-express-ktor/gradle.properties @@ -1,4 +1,4 @@ -ktor_version=3.2.3 -kotlin_version=2.1.20 +ktor_version=3.3.0 +kotlin_version=2.2.10 logback_version=1.5.6 kotlin.code.style=official diff --git a/codeSnippets/snippets/proguard/build.gradle.kts b/codeSnippets/snippets/proguard/build.gradle.kts index e8cc1b3ff..7d2bed59d 100644 --- a/codeSnippets/snippets/proguard/build.gradle.kts +++ b/codeSnippets/snippets/proguard/build.gradle.kts @@ -17,7 +17,7 @@ buildscript { plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/request-validation/build.gradle.kts b/codeSnippets/snippets/request-validation/build.gradle.kts index a545c1592..e7c2b5bf0 100644 --- a/codeSnippets/snippets/request-validation/build.gradle.kts +++ b/codeSnippets/snippets/request-validation/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/resource-routing/build.gradle.kts b/codeSnippets/snippets/resource-routing/build.gradle.kts index dc9a7d783..205c5b17d 100644 --- a/codeSnippets/snippets/resource-routing/build.gradle.kts +++ b/codeSnippets/snippets/resource-routing/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/server-sse/build.gradle.kts b/codeSnippets/snippets/server-sse/build.gradle.kts index 7271f9bee..4c036ecf4 100644 --- a/codeSnippets/snippets/server-sse/build.gradle.kts +++ b/codeSnippets/snippets/server-sse/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/server-websockets-serialization/build.gradle.kts b/codeSnippets/snippets/server-websockets-serialization/build.gradle.kts index 6db01dd8c..25f6bf667 100644 --- a/codeSnippets/snippets/server-websockets-serialization/build.gradle.kts +++ b/codeSnippets/snippets/server-websockets-serialization/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts b/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts index 0aead6daf..7b77d9862 100644 --- a/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts +++ b/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts @@ -5,8 +5,8 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" - kotlin("plugin.serialization").version("2.1.20") + id("io.ktor.plugin") version "3.3.0" + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/session-cookie-client/build.gradle.kts b/codeSnippets/snippets/session-cookie-client/build.gradle.kts index 874325faa..b2bc87ba1 100644 --- a/codeSnippets/snippets/session-cookie-client/build.gradle.kts +++ b/codeSnippets/snippets/session-cookie-client/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/session-cookie-server/build.gradle.kts b/codeSnippets/snippets/session-cookie-server/build.gradle.kts index 874325faa..b2bc87ba1 100644 --- a/codeSnippets/snippets/session-cookie-server/build.gradle.kts +++ b/codeSnippets/snippets/session-cookie-server/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/session-header-server/build.gradle.kts b/codeSnippets/snippets/session-header-server/build.gradle.kts index 874325faa..b2bc87ba1 100644 --- a/codeSnippets/snippets/session-header-server/build.gradle.kts +++ b/codeSnippets/snippets/session-header-server/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - kotlin("plugin.serialization").version("2.1.20") + kotlin("plugin.serialization").version("2.2.10") } application { diff --git a/codeSnippets/snippets/static-files/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/static-files/src/main/kotlin/com/example/Application.kt index 9303420f6..c8b1d04bf 100644 --- a/codeSnippets/snippets/static-files/src/main/kotlin/com/example/Application.kt +++ b/codeSnippets/snippets/static-files/src/main/kotlin/com/example/Application.kt @@ -3,7 +3,12 @@ package com.example import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.http.content.* +import io.ktor.server.plugins.conditionalheaders.* +import io.ktor.server.response.respond +import io.ktor.server.response.respondFile +import io.ktor.server.response.respondRedirect import io.ktor.server.routing.* +import io.ktor.util.date.GMTDate import java.io.* fun Application.module() { @@ -19,6 +24,14 @@ fun Application.module() { staticFiles("/files", File("textFiles")) { default("html-file.txt") exclude { file -> file.path.contains("excluded") } + fallback { requestedPath, call -> + when { + requestedPath.endsWith(".php") -> call.respondRedirect("/static/index.html") // absolute path + requestedPath.endsWith(".kt") -> call.respondRedirect("Default.kt") // relative path + requestedPath.endsWith(".xml") -> call.respond(HttpStatusCode.Gone) + else -> call.respondFile(File("files/index.html")) + } + } contentType { file -> when (file.name) { "html-file.txt" -> ContentType.Text.Html @@ -32,6 +45,15 @@ fun Application.module() { } } } + + staticFiles("/filesWithEtagAndLastModified", filesDir) { + etag { resource -> EntityTagVersion("etag") } + lastModified { resource -> GMTDate() } + } + + staticFiles("/filesWithStrongGeneratedEtag", filesDir) { + etag(ETagProvider.StrongSha256) + } } } diff --git a/codeSnippets/snippets/tomcat-war-ssl/build.gradle.kts b/codeSnippets/snippets/tomcat-war-ssl/build.gradle.kts index 9b6a84f04..f29f1b439 100644 --- a/codeSnippets/snippets/tomcat-war-ssl/build.gradle.kts +++ b/codeSnippets/snippets/tomcat-war-ssl/build.gradle.kts @@ -5,7 +5,7 @@ val slf4j_version: String by project plugins { application kotlin("jvm") - id("org.gretty") version "4.0.3" + id("org.gretty") version "4.1.7" id("war") } diff --git a/codeSnippets/snippets/tutorial-client-kmm/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-client-kmm/gradle/libs.versions.toml index 5dbb07aaf..c9cdfb73b 100644 --- a/codeSnippets/snippets/tutorial-client-kmm/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-client-kmm/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] agp = "8.3.1" -kotlin = "2.1.20" +kotlin = "2.2.10" coroutines = "1.9.0" -ktor = "3.2.3" +ktor = "3.3.0" compose = "1.6.8" compose-material3 = "1.2.1" androidx-activityCompose = "1.8.2" diff --git a/codeSnippets/snippets/tutorial-full-stack-task-manager/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-full-stack-task-manager/gradle/libs.versions.toml index 8a0f1b7e6..322cb239d 100644 --- a/codeSnippets/snippets/tutorial-full-stack-task-manager/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-full-stack-task-manager/gradle/libs.versions.toml @@ -13,7 +13,7 @@ androidx-material = "1.12.0" androidx-test-junit = "1.2.1" compose-plugin = "1.6.11" junit = "4.13.2" -kotlin = "2.1.20" +kotlin = "2.2.10" kotlinx-coroutines = "1.8.1" ktor = "2.3.12" logback = "1.5.18" diff --git a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml index 07f719d5d..a6472dbfa 100644 --- a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml @@ -2,8 +2,8 @@ [versions] exposed-version = "0.56.0" h2-version = "2.3.232" -kotlin-version = "2.1.20" -ktor-version = "3.2.3" +kotlin-version = "2.2.10" +ktor-version = "3.3.0" logback-version = "1.5.18" postgres-version = "42.7.4" diff --git a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts index 64cc8df61..6913bfeda 100644 --- a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts @@ -8,8 +8,8 @@ val h2_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" - id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" + id("io.ktor.plugin") version "3.3.0" + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.10" } group = "com.example" diff --git a/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml b/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml index 8c93b4c92..0b5f1b8ab 100644 --- a/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml +++ b/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml @@ -8,9 +8,9 @@ tutorial-server-get-started-maven tutorial-server-get-started-maven - 3.2.3 + 3.3.0 official - 2.1.20 + 2.2.10 1.5.18 2.0.12 UTF-8 diff --git a/codeSnippets/snippets/tutorial-server-get-started/build.gradle.kts b/codeSnippets/snippets/tutorial-server-get-started/build.gradle.kts index e99a32149..59bff8151 100644 --- a/codeSnippets/snippets/tutorial-server-get-started/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-get-started/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts b/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts index b8b8c9e26..a387523a2 100644 --- a/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts @@ -5,8 +5,8 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" - id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" + id("io.ktor.plugin") version "3.3.0" + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.10" } application { diff --git a/codeSnippets/snippets/tutorial-server-routing-and-requests/build.gradle.kts b/codeSnippets/snippets/tutorial-server-routing-and-requests/build.gradle.kts index 395c76f9f..d2d4890c9 100644 --- a/codeSnippets/snippets/tutorial-server-routing-and-requests/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-routing-and-requests/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/tutorial-server-web-application/build.gradle.kts b/codeSnippets/snippets/tutorial-server-web-application/build.gradle.kts index 50b0fafae..e8eec4c6c 100644 --- a/codeSnippets/snippets/tutorial-server-web-application/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-web-application/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts index 6b6c96db0..a5644a5f7 100644 --- a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts @@ -4,8 +4,8 @@ val logback_version: String by project plugins { kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" - id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" + id("io.ktor.plugin") version "3.3.0" + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.10" } group = "com.example" diff --git a/codeSnippets/snippets/tutorial-website-static/build.gradle.kts b/codeSnippets/snippets/tutorial-website-static/build.gradle.kts index f43a4d860..fdd43f4aa 100644 --- a/codeSnippets/snippets/tutorial-website-static/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-website-static/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.2.3" + id("io.ktor.plugin") version "3.3.0" } application { diff --git a/help-versions.json b/help-versions.json index 82b760540..bbfe69570 100644 --- a/help-versions.json +++ b/help-versions.json @@ -10,7 +10,7 @@ "isCurrent": false }, { - "version": "3.2.3", + "version": "3.3.0", "url": "/docs/", "isCurrent": true } diff --git a/ktor.tree b/ktor.tree index 1f81cd006..bc8020eb5 100644 --- a/ktor.tree +++ b/ktor.tree @@ -293,6 +293,7 @@ + @@ -383,11 +384,13 @@ + + diff --git a/labels.list b/labels.list index 3c70821f3..e4138ff33 100644 --- a/labels.list +++ b/labels.list @@ -12,6 +12,7 @@ This feature is experimental. It may be dropped or changed at any time. Opt-in is required (see details below). + Server Work in progress Beta diff --git a/project.ihp b/project.ihp index 6818884fc..358cf2ada 100644 --- a/project.ihp +++ b/project.ihp @@ -14,7 +14,7 @@ diff --git a/topics/client-server-sent-events.topic b/topics/client-server-sent-events.topic index 1d7695972..c8c30e282 100644 --- a/topics/client-server-sent-events.topic +++ b/topics/client-server-sent-events.topic @@ -50,11 +50,8 @@ class.

- -

️️Not supported in: OkHttp

-

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

@@ -78,6 +75,63 @@
+ +

+ SSE responses are streaming by nature, which makes capturing the full body impractical. You can enable + a diagnostic buffer to safely retrieve the response body when an SSE stream fails. The buffer only + contains data that has already been processed (no re-reading from the network) and is intended for + logging and error analysis in case of failures. +

+ + install(SSE) { + bufferPolicy = SSEBufferPolicy.LastEvents(10) + } + +

+ You can also configure the buffer per call: +

+ + client.sse(url, { + bufferPolicy(SSEBufferPolicy.All) + }) { + // ... + } + + +

+ The SSEBufferPolicy type provides several strategies for storing processed SSE data. + These policies control how much of the stream is retained in memory and made available in case of + errors. +

+ + + <code>Off</code> (default) + No buffering. + + + <code>LastLines(n)</code> + Keeps the last n lines. + + + <code>LastEvent</code> + Keeps the last completed SSE event. + + + <code>LastEvents(n)</code> + Keeps the last n completed SSE events. + + + <code>All</code> + Keeps all processed events so far. + Use with caution for long-lived streams. + + +

+ On failure, you can access the buffer by using response?.bodyAsText() without + re-reading from the network. +

+
+

diff --git a/topics/client-webrtc.md b/topics/client-webrtc.md new file mode 100644 index 000000000..4449c02e4 --- /dev/null +++ b/topics/client-webrtc.md @@ -0,0 +1,231 @@ +[//]: # (title: WebRTC client) + + + + + + +

+ Required dependencies: io.ktor:%artifact_name% +

+

+ Supported platforms: JS/Wasm, Android +

+

+ Code example: ktor-chat +

+ + + The WebRTC client enables real-time peer-to-peer communication in multiplatform projects. + + +Web Real-Time Communication (WebRTC) is a set of standards and APIs for real-time, peer-to-peer communication in +browsers and native apps. + +The WebRTC client in Ktor enables real-time peer-to-peer communication in multiplatform projects. With WebRTC, you can +build features such as: + +- Video and voice calls +- Multiplayer games +- Collaborative applications (whiteboards, editors, etc.) +- Low-latency data exchange between clients + +## Add dependencies {id="add-dependencies"} + +To use `WebRtclient`, you need to include the `%artifact_name%` artifact in the build script: + + + +## Create a client + +When creating a `WebRtcClient`, choose an engine based on your target platform: + +- JS/Wasm: `JsWebRtc` – uses browser `RTCPeerConnection` and media devices. +- Android: `AndroidWebRtc` – uses `PeerConnectionFactory` and Android media APIs. + +You can then provide platform-specific configuration similar to `HttpClient`. STUN/TURN servers are required for +[ICE](#ice) to work correctly. You can use existing solutions such as [coturn](https://github.com/coturn/coturn): + + + + +```kotlin +val jsClient = WebRtcClient(JsWebRtc) { + defaultConnectionConfig = { + iceServers = listOf(WebRtc.IceServer("stun:stun.l.google.com:19302")) + } +} +``` + + + + +```kotlin +val androidClient = WebRtcClient(AndroidWebRtc) { + context = appContext // Required: provide Android context + defaultConnectionConfig = { + iceServers = listOf(WebRtc.IceServer("stun:stun.l.google.com:19302")) + } +} +``` + + + + +## Create a connection and negotiate SDP + +After creating a `WebRtcClient`, the next step is to create a peer connection. +A peer connection is the core object that manages the real-time communication between two clients. + +To establish a connection, WebRTC uses the Session Description Protocol (SDP). This involves three steps: + +1. One peer (the caller) creates an offer. +2. The other peer (the callee) responds with an answer. +3. Both peers apply each other’s descriptions to complete the setup. + +```kotlin +// Caller creates a connection and an offer +val caller = jsClient.createPeerConnection() +val offer = caller.createOffer() +caller.setLocalDescription(offer) +// send offer.sdp to the remote peer via your signaling mechanism + +// Callee receives the offer and creates an answer +val callee = jsClient.createPeerConnection() +callee.setRemoteDescription( + WebRtc.SessionDescription(WebRtc.SessionDescriptionType.OFFER, remoteOfferSdp) +) +val answer = callee.createAnswer() +callee.setLocalDescription(answer) +// send answer.sdp back to the caller via signaling + +// Caller applies the answer +caller.setRemoteDescription( + WebRtc.SessionDescription(WebRtc.SessionDescriptionType.ANSWER, remoteAnswerSdp) +) +``` + +## Exchange ICE candidates {id="ice"} + +Once SDP negotiation is complete, peers still need to discover how to connect across networks. [Interactive Connectivity +Establishment (ICE)](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) allows peers to find network +paths to each other. + +- Each peer gathers its own ICE candidates. +- These candidates must be sent to the other peer through your chosen signaling channel. +- Once both peers add each other’s candidates, the connection can succeed. + +```kotlin +// Collect and send local candidates +scope.launch { + caller.iceCandidates.collect { candidate -> + // send candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex to remote peer + } +} + +// Receive and add remote candidates +callee.addIceCandidate(WebRtc.IceCandidate(candidateString, sdpMid, sdpMLineIndex)) + +// Optionally wait until all candidates are gathered +callee.awaitIceGatheringComplete() +``` + +> Ktor does not provide signaling. Use WebSockets, HTTP, or another transport to exchange offers, answers, and +> ICE candidates. +> +{style="note"} + +## Use a data channel + +WebRTC supports data channels, which let peers exchange arbitrary messages. This is useful for chat, multiplayer games, +collaborative tools, or any low-latency messaging between clients. + +### Creating a channel + +To create a channel on one side, use the `.createDataChannel()` method: + +```kotlin +val channel = caller.createDataChannel("chat") +``` + +You can then listen for data channel events on the other side: + +```kotlin +scope.launch { + callee.dataChannelEvents.collect { event -> + when (event) { + is DataChannelEvent.Open -> println("Channel opened: ${event.channel}") + is DataChannelEvent.Closed -> println("Channel closed") + else -> {} + } + } +} +``` + +### Sending and receiving messages + +Channels use a `Channel`-like API, familiar to Kotlin developers: + +```kotlin +// Send a message +scope.launch { channel.send("hello") } + +// Receive messages +scope.launch { println("received: " + channel.receiveText()) } +``` + +## Add and observe media tracks + +In addition to data channels, WebRTC supports media tracks for audio and video. This allows you to build applications +such as video calls or screen sharing. + +### Creating local tracks + +You can request audio or video tracks from local devices (microphone, camera): + +```kotlin +val audioConstraints = WebRtcMedia.AudioTrackConstraints( + echoCancellation = true +) +val videoConstraints = WebRtcMedia.VideoTrackConstraints( + width = 1280, + height = 720 +) +val audio = rtcClient.createAudioTrack(audioConstraints) +val video = rtcClient.createVideoTrack(videoConstraints) + +val pc = jsClient.createPeerConnection() +pc.addTrack(audio) +pc.addTrack(video) +``` + +On the web, this uses `navigator.mediaDevices.getUserMedia`. On Android, it uses the Camera2 API and you must request +microphone/camera permissions manually. + +### Receiving remote tracks + +You can also listen for remote media tracks: + +```kotlin +scope.launch { + pc.trackEvents.collect { event -> + when (event) { + is TrackEvent.Add -> println("Remote track added: ${event.track.id}") + is TrackEvent.Remove -> println("Remote track removed: ${event.track.id}") + } + } +} +``` + +## Limitations + +The WebRTC client is experimental and has the following limitations: + +- Signaling is not included. You need to implement your own signaling (for example, with WebSockets or HTTP). +- Supported platforms are JavaScript/Wasm and Android. iOS, JVM desktop, and Kotlin/Native support are planned in future + releases. +- Permissions must be handled by your application. Browsers prompt users for microphone and camera access, while + Android requires runtime permission requests. +- Only basic audio and video tracks are supported. Screen sharing, device selection, simulcast, and advanced RTP + features are not yet available. +- Connection statistics are available but differ across platforms and do not follow a unified schema. \ No newline at end of file diff --git a/topics/openapi-spec-generation.md b/topics/openapi-spec-generation.md new file mode 100644 index 000000000..f3f7a0114 --- /dev/null +++ b/topics/openapi-spec-generation.md @@ -0,0 +1,143 @@ +[//]: # (title: OpenAPI specification generation) + + + + + + +

+Code example: +openapi +

+
+ +Ktor provides experimental support for generating OpenAPI specifications directly from your Kotlin code. +This functionality is available via the Ktor Gradle plugin and can be combined with the [OpenAPI](server-openapi.md) +and [SwaggerUI](server-swagger-ui.md) plugins to serve interactive API documentation. + +> The OpenAPI Gradle extension requires Kotlin 2.2.20. Using other versions may result in compilation +> errors. +> +{style="note"} + +## Add the Gradle plugin + +To enable specification generation, apply the Ktor Gradle plugin to your project: + +```kotlin +plugins { + id("io.ktor.plugin") version "%ktor_version%" +} +``` + +## Configure the extension + +To configure the extension, use the `openApi` block inside the `ktor` extension in your +build.gradle.kts +file. You can provide metadata such as title, description, license, and contact information: + +```kotlin +ktor { + @OptIn(OpenApiPreview::class) + openApi { + title = "OpenAPI example" + version = "2.1" + summary = "This is a sample API" + description = "This is a longer description" + termsOfService = "https://example.com/terms/" + contact = "contact@example.com" + license = "Apache/1.0" + + // Location of the generated specification (defaults to openapi/generated.json) + target = project.layout.buildDirectory.file("open-api.json") + } +} +``` + +## Routing API introspection + +The plugin can analyze your server routing DSL to infer basic path information, such as: + +- The merged path (`/api/v1/users/{id}`). +- Path parameters. +- HTTP methods (such as `GET` and `POST`). + +```kotlin +routing { + route("/api/v1") { + get("/users") { } + get("/users/{id}") { } + post("/users") { } + } +} +``` + +Because request parameters and responses are handled inside route lambdas, the plugin cannot infer detailed +request/response schemas automatically. To generate a complete and useful specification, you can use annotations. + +## Annotate routes + +To enrich the specification, Ktor uses a KDoc-like annotation API. Annotations provide metadata that cannot be inferred +from code and integrate seamlessly with existing routes. + +```kotlin +/** + * Get a single user. + * + * @path id The ID of the user + * @response 404 The user was not found + * @response 200 [User] The user. + */ +get("/api/users/{id}") { + val user = repository.get(call.parameters["id"]!!) + ?: return@get call.respond(HttpStatusCode.NotFound) + call.respond(user) +} + +``` + +### Supported KDoc fields + +| Tag | Format | Description | +|-----------------|-------------------------------------------------|-------------------------------------------------| +| `@tags` | `@tags *name` | Associates the endpoint with a tag for grouping | +| `@path` | `@path [Type] name description` | Describes a path parameter | +| `@query` | `@query [Type] name description` | Query parameter | +| `@header` | `@header [Type] name description` | Header parameter | +| `@cookie` | `@cookie [Type] name description` | Cookie parameter | +| `@body` | `@body contentType [Type] description` | Request body | +| `@response` | `@response code contentType [Type] description` | Response with optional type | +| `@deprecated` | `@deprecated reason` | Marks endpoint deprecated | +| `@description` | `@description text` | Extended description | +| `@security` | `@security scheme` | Security requirements | +| `@externalDocs` | `@external href` | External documentation links | + + +## Generate the specification + +To generate the OpenAPI specification, run the following Gradle task: + +```shell +./gradlew buildOpenApi +``` + +This task runs the Kotlin compiler with a custom plugin that analyzes your routing code and produces a +JSON specification. + +> Some constructs cannot be evaluated at compile time. The generated specification may be incomplete. Improvements are +> planned for later Ktor releases. +> +{style="note"} + +## Serve the specification + +To make the generated specification available at runtime, you can use the [OpenAPI](server-openapi.md) +or [SwaggerUI](server-swagger-ui.md) plugins. + +The following example serves the generated specification file at an OpenAPI endpoint: + +```kotlin +routing { + openAPI("/docs", swaggerFile = "openapi/generated.json") +} +``` \ No newline at end of file diff --git a/topics/releases.md b/topics/releases.md index 1077aa75d..d8b1eb65f 100644 --- a/topics/releases.md +++ b/topics/releases.md @@ -33,6 +33,15 @@ The following table lists details of the latest Ktor releases. +
VersionRelease DateHighlights
3.3.0September 11, 2025 +

+A minor release that introduces major features like experimental OpenAPI generation preview, improved static content +handling, WebRTC client for Android and JS/Wasm, and upgrades to Jetty, OkHttp, and Kotlin 2.2. For more information, +see . +

+ + +
3.2.3July 29, 2025

A patch release that introduces improvements to YAML config handling, DI resolution, and Wasm/JS stability, along with diff --git a/topics/server-auto-reload.topic b/topics/server-auto-reload.topic index 4bd630f1d..db7b07b40 100644 --- a/topics/server-auto-reload.topic +++ b/topics/server-auto-reload.topic @@ -40,7 +40,58 @@

+ + Auto-reload works only for specific module declarations. The following table shows support across versions: + + + + + + + + + + + + + + + + + + + + + + + + + + +
Module type<= 3.2> 3.2
Lambda initializer❌ Not supported❌ Not supported
Blocking function reference❌ Not supported✅ Supported
Suspend function reference✅ Supported❌ Not supported
Config reference✅ Supported✅ Supported
+ + + // Suspend function reference + embeddedServer(Netty, port = 8080, module = Application::mySuspendModule) + // Configuration reference + ktor { + application { + modules = [ com.example.ApplicationKt.mySuspendModule ] + } + } + + + + + // Lambda + embeddedServer(Netty, port = 8080) { configureServer() } + + // Blocking function reference + embeddedServer(Netty, port = 8080, module = Application::myBlockingModule) + + +

To use Auto-reload, you need to enable the @@ -57,7 +108,7 @@

  • If you run a server using embeddedServer, you can use the - io.ktor.development + io.ktor.development system property.

  • @@ -69,7 +120,6 @@

    -

    When you enable development mode, diff --git a/topics/server-http2.md b/topics/server-http2.md index b317e3e47..1fc644dd8 100644 --- a/topics/server-http2.md +++ b/topics/server-http2.md @@ -49,9 +49,6 @@ The next step is configuring Ktor to use your keystore. See the example `applica - - - ## ALPN implementation {id="apln_implementation"} HTTP/2 requires ALPN ([Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation)) to be enabled. The first option is to use an external ALPN implementation that needs to be added to the boot classpath. @@ -78,3 +75,24 @@ The example below shows how to add a native implementation (statically linked Bo `tc.native.classifier` should be one of the following: `linux-x86_64`, `osx-x86_64`, or `windows-x86_64`. The [http2-netty](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/http2-netty) runnable example demonstrates how to enable HTTP/2 support for Netty. + +#### HTTP/2 without TLS + +The Netty engine also supports [HTTP/2 over cleartext (h2c)](https://httpwg.org/specs/rfc7540.html#discover-http). +This allows HTTP/2 communication without TLS, typically within private networks where encryption is not required. +Clients can initiate communication with an HTTP/1.1 request and then upgrade to HTTP/2. + +To enable h2c, set the `enableH2c` flag to `true` in the engine configuration: + +```kotlin +embeddedServer(Netty, configure = { + connector { + port = 8080 + } + enableHttp2 = true + enableH2c = true +}) +``` + + +Note that h2c requires `enableHttp2 = true` and cannot be used if an SSL connector is configured on the server. \ No newline at end of file diff --git a/topics/server-openapi.md b/topics/server-openapi.md index 286894885..404b365d1 100644 --- a/topics/server-openapi.md +++ b/topics/server-openapi.md @@ -19,10 +19,9 @@ The OpenAPI plugin allows you to generate OpenAPI documentation for your project. -Ktor allows you to generate and serve OpenAPI documentation for your project based on the existing OpenAPI specification. - - - +Ktor allows you to generate and serve OpenAPI documentation for your project based on an existing OpenAPI specification. +You can serve an existing YAML or JSON specification, or generate one using the +[OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. ## Add dependencies {id="add_dependencies"} diff --git a/topics/server-static-content.md b/topics/server-static-content.md index bc4d0f07f..2904a72fc 100644 --- a/topics/server-static-content.md +++ b/topics/server-static-content.md @@ -19,8 +19,8 @@ stylesheets, scripts, or images. While it is certainly possible with Ktor to load the contents of a file and [send it in a response](server-responses.md) to a client, Ktor simplifies this process by providing additional functions for serving static content. -With Ktor, you can serve content from [folders](#folders),[ZIP files](#zipped) -, and [embedded application resources](#resources). +With Ktor, you can serve content from [folders](#folders), [ZIP files](#zipped), +and [embedded application resources](#resources). ## Folders {id="folders"} @@ -31,7 +31,7 @@ function. In this case, relative paths are resolved using the current working di ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="10-11,35"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="15-16,57"} In the example above, any request from `/resources` is mapped to the `files` physical folder in the current working directory. @@ -57,7 +57,7 @@ In this example, any request from the root URL `/` is mapped directly to the con The `staticZip()` function also supports automatic reloading. If any changes are detected in the ZIP file's parent directory, the ZIP file system is reloaded on the next request. This ensures that the served content remains -up-to-date without requiring a server restart. +up to date without requiring a server restart. For the full example, see [static-zip](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/static-zip). @@ -74,7 +74,7 @@ function. {src="snippets/static-resources/src/main/kotlin/com/example/Application.kt" include-lines="8,9,17"} This maps any request from `/resources` to the `static` package in application resources. -In this case, Ktor recursively serves up any file from the `static` package as long as a URL path and a path to resource +In this case, Ktor recursively serves up any file from the `static` package as long as a URL path and a path-to-resource match. For the full example, @@ -104,7 +104,7 @@ To use this functionality, define the `preCompressed()` function inside a block ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="12,14,18"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="17,19,23"} In this example, for a request made to `/js/script.js`, Ktor can serve `/js/script.js.br` or `/js/script.js.gz`. @@ -120,29 +120,29 @@ static route that has a `GET` defined. ### Default file response {id="default-file"} -The `default()` function provides the ability to reply with a file for any request inside static route that has no +The `default()` function provides the ability to reply with a file for any request inside a static route that has no corresponding file. ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="12-13,18"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="17-18,23"} In this example when the client requests a resource that doesn't exist, the `index.html` file will be served as a response. ### Content type {id="content-type"} -By default, Ktor tries to guess value of the `Content-Type` header from the file extension. You can use +By default, Ktor tries to guess the value of the `Content-Type` header from the file extension. You can use the `contentType()` function to set the `Content-Type` header explicitly. ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="19,22-27,34"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="24,35-40,47"} -In this example, the response for file `html-file.txt` will have `Content-Type: text/html` header and for every other -file default behaviour will be applied. +In this example, the response for the file `html-file.txt` will have the `Content-Type: text/html` header, and for every +other file the default behavior will be applied. ### Caching {id="caching"} @@ -151,7 +151,28 @@ The `cacheControl()` function allows you to configure the `Cache-Control` header ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="9-10,19,28-36,38-40"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="14-15,24,41-47,57-58,60-62"} + +When the [`ConditionalHeaders`](server-conditional-headers.md) plugin is installed, Ktor can serve static resources with +`ETag` and `LastModified` headers and process conditional headers to avoid sending the body of content if it hasn't changed +since the last request: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="49-52"} + +In this example, the `etag` and `lastModified` values are calculated dynamically based on each resource and applied to the response. + +To simplify `ETag` generation, you can also use a predefined provider: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="54-56"} + +In this example, a strong `ETag` is generated using the SHA‑256 hash of the resource content. +If an I/O error occurs, no `ETag` is generated. > For more information on caching in Ktor, see [Caching headers](server-caching-headers.md). > @@ -165,7 +186,7 @@ the server will respond with a `403 Forbidden` status code. ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="19,21,34"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="24,26,47"} ### File extensions fallbacks {id="extensions"} @@ -178,6 +199,22 @@ When a requested file is not found, Ktor can add the given extensions to the fil In this example, when `/index` is requested, Ktor will search for `/index.html` and serve the found content. +### Custom fallback + +To configure custom fallback behavior when a requested static resource is not found, use the `fallback()` function. +With `fallback()`, you can inspect the requested path and decide how to respond. For example, you might redirect to +another resource, return a specific HTTP status, or serve an alternative file. + +You can add `fallback()` inside `staticFiles()`, `staticResources()`, `staticZip()`, or `staticFileSystem()`. The callback provides +the requested path and the current `ApplicationCall`. + +The example below shows how to redirect certain extensions, return a custom status, or fall back to `index.html`: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="24,27-34,47"} + ### Custom modifications {id="modify"} The `modify()` function allows you to apply custom modification to a resulting response. @@ -185,7 +222,7 @@ The `modify()` function allows you to apply custom modification to a resulting r ```kotlin ``` -{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="12,15-18"} +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="17,20-23"} ## Handle errors {id="errors"} diff --git a/topics/server-swagger-ui.md b/topics/server-swagger-ui.md index 9567a47d4..1b1736fbb 100644 --- a/topics/server-swagger-ui.md +++ b/topics/server-swagger-ui.md @@ -20,14 +20,8 @@ The SwaggerUI plugin allows you to generate Swagger UI for your project. Ktor allows you to generate and serve Swagger UI for your project based on the existing OpenAPI specification. -With Swagger UI, you can visualize and interact with the API resources. - -> The following tools are available for generating OpenAPI definitions from code and vice versa: -> - The [Ktor plugin](https://www.jetbrains.com/help/idea/ktor.html#openapi) for IntelliJ IDEA provides the ability to generate OpenAPI documentation for server-side Ktor applications. -> - The [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator) allows you to create a Ktor project from your API definitions by using the [kotlin-server](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin-server.md) generator. Alternatively, you can use IntelliJ IDEA's [functionality](https://www.jetbrains.com/help/idea/openapi.html#codegen). -> -{id="open-api-note"} - +With Swagger UI, you can visualize and interact with the API resources. You can serve an existing YAML or JSON +specification, or generate one using the [OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. ## Add dependencies {id="add_dependencies"} diff --git a/topics/server-war.md b/topics/server-war.md index e1d43300b..522bbc9c9 100644 --- a/topics/server-war.md +++ b/topics/server-war.md @@ -90,6 +90,11 @@ Then, configure the URL pattern for this servlet: ## Configure Gretty {id="configure-gretty"} +> Ktor 3.3.0 requires Jetty 12, which is not yet supported by Gretty. If you rely on Gretty for development or +> deployment, use Ktor 3.2.3 instead until Gretty adds Jetty 12 support. +> +{style="warning"} + The [Gretty](https://plugins.gradle.org/plugin/org.gretty) plugin allows you to [run](#run) a servlet application on Jetty and Tomcat. To install this plugin, open the `build.gradle.kts` file and add the following code to the `plugins` block: ```groovy diff --git a/topics/whats-new-320.md b/topics/whats-new-320.md index db49f3181..4b45ef7d9 100644 --- a/topics/whats-new-320.md +++ b/topics/whats-new-320.md @@ -17,6 +17,11 @@ Here are the highlights for this feature release: Starting with Ktor 3.2.0, [application modules](server-modules.md) have support for suspendable functions. +> With the introduction of suspend module support, auto-reload in development mode no longer works with blocking +> function references. For more information, see [](#regression). +> +{style="warning"} + Previously, adding asynchronous functions inside Ktor modules required the `runBlocking` block that could lead to a deadlock on server creation: @@ -274,6 +279,26 @@ This simplifies working with structured configuration and supports automatic par For more information and advanced usage, see [](server-dependency-injection.md). +### Development mode auto-reload regression {id="regression"} + +As a side effect to the support of suspending functions, blocking function references (`Application::myModule`) are now +wrapped into anonymous inner classes during casting. This breaks auto-reload, because the function name is no longer +retained as a stable reference. + +This means that auto-reload in `development` mode only works with suspend function modules and configuration references: + +```kotlin +// Suspend function reference +embeddedServer(Netty, port = 8080, module = Application::mySuspendModule) + +// Configuration reference +ktor { + application { + modules = [ com.example.ApplicationKt.mySuspendModule ] + } +} +``` + ## Ktor Client ### `SaveBodyPlugin` and `HttpRequestBuilder.skipSavingBody()` are deprecated diff --git a/topics/whats-new-330.md b/topics/whats-new-330.md new file mode 100644 index 000000000..68efb909c --- /dev/null +++ b/topics/whats-new-330.md @@ -0,0 +1,226 @@ +[//]: # (title: What's new in Ktor 3.3.0) + + + +_[Released: September 11, 2025](releases.md#release-details)_ + +Ktor 3.3.0 delivers new capabilities across server, client, and tooling. Here are the highlights for this feature +release: + +* [Custom fallback mechanism for static resources](#custom-fallback) +* [OpenAPI specification generation](#openapi-spec-gen) +* [HTTP/2 cleartext (h2c) support](#http2-h2c-support) +* [Experimental WebRTC client](#webrtc-client) + +## Ktor Server + +### Custom fallback for static resources {id="custom-fallback"} + +Ktor 3.3.0 introduces a new `fallback()` function for static content, allowing you to define custom +behavior when a requested resource is not found. + +Unlike `default()`, which always serves the same fallback file, `fallback()` gives you access to the original requested +path and the current `ApplicationCall`. You can use this to redirect, return custom status codes, or serve different +files dynamically. + +To define custom fallback behaviour, use the `fallback()` function within `staticFiles()`, `staticResources()`, `staticZip()`, or +`staticFileSystem()`: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="24,27-34,47"} + +### LastModified and Etag headers for static content + +Ktor 3.3.0 introduces support for `ETag` and `LastModified` headers for static resources. When the [`ConditionalHeaders`](server-conditional-headers.md) +plugin is installed, you can process conditional headers to avoid sending the body of content if it hasn't changed since +the last request: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="49-52"} + +The values are calculated dynamically based on each resource and applied to the response. + +You can also use a predefined provider, for example to generate a strong `ETag` using the SHA‑256 hash of the resource content: + +```kotlin +``` + +{src="snippets/static-files/src/main/kotlin/com/example/Application.kt" include-lines="54-56"} + +### Development mode auto-reload limitations + +In Ktor 3.2.0, the introduced [support for suspend module functions](whats-new-320.md#suspendable-module-functions) +caused a regression where auto-reload stopped working for applications that use blocking module references. + +This regression remains in 3.3.0 and auto-reload continues to work only with `suspend` function modules and +configuration references. + +Examples of supported module declarations: + +```kotlin +// Supported — suspend function reference +embeddedServer(Netty, port = 8080, module = Application::mySuspendModule) + +// Supported — configuration reference (application.conf / application.yaml) +ktor { + application { + modules = [ com.example.ApplicationKt.mySuspendModule ] + } +} +``` + +We plan to restore support for blocking function references in a future release. Until then, prefer a `suspend` module +or configuration reference in `development` mode. + +### HTTP/2 cleartext (h2c) support {id="http2-h2c-support"} + +Ktor 3.3.0 introduces support for HTTP/2 over cleartext (h2c) for the Netty engine, +which allows HTTP/2 communication without TLS encryption. +This setup is typically used in trusted environments, such as local testing or private networks. + +To enable h2c, set the `enableH2c` flag to true in the engine configuration. +For more information, see [HTTP/2 without TLS](server-http2.md#http-2-without-tls). + +## Ktor Client + +### SSE response body buffer + +Until now, attempting to call `response.bodyAsText()` after an SSE error failed due to double-consume issues. + +Ktor 3.3.0 introduces a configurable diagnostic buffer that allows you to capture already-processed SSE data for +debugging and error handling. + +You can configure the buffer globally when installing the [SSE plugin](client-server-sent-events.topic): + +```kotlin +install(SSE) { + bufferPolicy = SSEBufferPolicy.LastEvents(10) +} +``` + +Or per call: + +```kotlin +client.sse(url, { bufferPolicy(SSEBufferPolicy.All) }) { + // … +} +``` + +As the SSE stream is consumed, the client maintains a snapshot of the processed data in an in-memory buffer (without +re-reading from the network). If an error occurs, you can safely call `response?.bodyAsText()` for logging or +diagnostics. + +For more information, see [Response buffering](client-server-sent-events.topic#response-buffering). + +### WebRTC client {id="webrtc-client"} + +This release introduces experimental WebRTC client support for peer-to-peer real-time communication in multiplatform +projects. + +WebRTC enables applications such as video calls, multiplayer gaming, and collaborative tools. With this release, you +can now use a unified Kotlin API to establish peer connections and exchange data channels across JavaScript/Wasm and +Android targets. Additional targets such as iOS, JVM desktop, and Native are planned for future releases. + +You can create a `WebRtcClient` by selecting an engine for your platform and providing configuration, similar to +`HttpClient`: + + + +Once created, the client can establish peer-to-peer connections using Interactive Connectivity Establishment (ICE). +After negotiation completes, peers can open data channels and exchange messages. + +```kotlin +val connection = client.createPeerConnection() + +// Add a remote ICE candidate (received via your signaling channel) +connection.addIceCandidate(WebRtc.IceCandidate(candidateString, sdpMid, sdpMLineIndex)) + +// Wait until all local candidates are gathered +connection.awaitIceGatheringComplete() + +// Listen for incoming data channel events +connection.dataChannelEvents.collect { event -> + when (event) { + is Open -> println("Another peer opened a chanel: ${event.channel}") + is Closed -> println("Data channel is closed") + is Closing, is BufferedAmountLow, is Error -> println(event) + } +} + +// Create a channel and send/receive messages +val channel = connection.createDataChannel("chat") +channel.send("hello") +val answer = channel.receiveText() +``` + +For more details on usage and limitations, see the [WebRTC client](client-webrtc.md) documentation. + +### Updated OkHttp version + +In Ktor 3.3.0, the Ktor client's `OkHttp` engine has been upgraded to use OkHttp 5.1.0 (previously 4.12.0). This major +version bump may introduce API changes for projects that interact directly with OkHttp. Such projects should verify +compatibility. + +### Unified OkHttp SSE session + +The OkHttp engine now uses the standard API for Server-Sent Events (SSE), +replacing the previously introduced `OkHttpSSESession`. +This change unifies SSE handling across all client engines and addresses the limitations of the OkHttp-specific implementation. + + +## Gradle plugin + +### OpenAPI specification generation {id="openapi-spec-gen"} + + +Ktor 3.3.0 introduces an experimental OpenAPI generation feature via the Gradle plugin and a compiler plugin. This +allows you to generate OpenAPI specifications directly from your application code at build time. + +It provides the following capabilities: +- Analyze Ktor route definitions and merge nested routes, local extensions, and resource paths. +- Parse preceding KDoc annotations to supply OpenAPI metadata, including: + - Path, query, header, cookie, and body parameters + - Response codes and types + - Security, descriptions, deprecations, and external documentation links +- Infer request and response bodies from `call.receive()` and `call.respond()`. + + + + +#### Generate the OpenAPI specification + +To generate the OpenAPI specification file from your Ktor routes and KDoc annotations, use the following command: + +```shell +./gradlew buildOpenApi +``` + +#### Serve the specification + +To make the generated specification available at runtime, you can then use the [OpenAPI](server-openapi.md) +or [SwaggerUI](server-swagger-ui.md) plugins. + +The following example serves the generated specification file at an OpenAPI endpoint: + +```kotlin +routing { + openAPI("/docs", swaggerFile = "openapi/generated.json") +} +``` + +For more details about this feature, see [OpenAPI specification generation](openapi-spec-generation.md). + + +## Shared + +### Updated Jetty version + +The Jetty server and client engines have been upgraded to use Jetty 12. For most applications, this upgrade is fully +backward-compatible, but client and server code now leverage the updated Jetty APIs internally. + +If your project uses Jetty APIs directly, be aware that there are breaking changes. For more details, refer to +[the official Jetty migration guide](https://jetty.org/docs/jetty/12.1/programming-guide/migration/11-to-12.html). diff --git a/v.list b/v.list index 2cb51eb1c..9a6264da7 100644 --- a/v.list +++ b/v.list @@ -4,8 +4,8 @@ - - + +