diff --git a/cfg/glossary.xml b/cfg/glossary.xml index d9ce1f684..34af650fb 100644 --- a/cfg/glossary.xml +++ b/cfg/glossary.xml @@ -4,4 +4,7 @@ A part of a URL that assigns values to specified parameters and starts with the ? character. + + A list of locations where the JVM looks for user classes and resources. + \ No newline at end of file diff --git a/codeSnippets/build.gradle b/codeSnippets/build.gradle index 33a66ee64..7c20e78f3 100644 --- a/codeSnippets/build.gradle +++ b/codeSnippets/build.gradle @@ -16,7 +16,7 @@ buildscript { } repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } configurations.classpath { @@ -70,14 +70,14 @@ allprojects { kotlin_version = rootProject.properties['kotlin_snapshot_version'] repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } } repositories { mavenCentral() maven { - url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" + url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") } } } @@ -89,7 +89,7 @@ def ktorRepositoryDir = file("$buildDir/m2") if (ktorRepositoryDir.exists()) { allprojects { repositories { - maven { url ktorRepositoryDir.absolutePath } + maven { url = uri(ktorRepositoryDir.absolutePath) } } } } else { diff --git a/codeSnippets/gradle.properties b/codeSnippets/gradle.properties index fbac9b95e..6bfb1c9d9 100644 --- a/codeSnippets/gradle.properties +++ b/codeSnippets/gradle.properties @@ -4,9 +4,11 @@ kotlin.code.style = official kotlin.native.binary.memoryModel = experimental # gradle configuration org.gradle.configureondemand = false +kotlin.mpp.applyDefaultHierarchyTemplate=false +org.gradle.java.installations.auto-download=false # versions -kotlin_version = 2.2.20 -ktor_version = 3.3.3 +kotlin_version = 2.3.0 +ktor_version = 3.4.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/settings.gradle.kts b/codeSnippets/settings.gradle.kts index 675bc3df3..742f8d072 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -161,6 +161,8 @@ module("snippets", "tutorial-server-restful-api") module("snippets", "tutorial-server-websockets") module("snippets", "tutorial-server-docker-compose") module("snippets", "htmx-integration") +module("snippets", "server-http-request-lifecycle") +module("snippets", "openapi-spec-gen") if(!System.getProperty("os.name").startsWith("Windows")) { module("snippets", "embedded-server-native") diff --git a/codeSnippets/snippets/_misc_client/Apache5Create.kt b/codeSnippets/snippets/_misc_client/Apache5Create.kt index c8fb11787..78a7233f8 100644 --- a/codeSnippets/snippets/_misc_client/Apache5Create.kt +++ b/codeSnippets/snippets/_misc_client/Apache5Create.kt @@ -11,11 +11,21 @@ val client = HttpClient(Apache5) { socketTimeout = 10_000 connectTimeout = 10_000 connectionRequestTimeout = 20_000 + + // Configure the Apache5 ConnectionManager + configureConnectionManager { + setMaxConnPerRoute(1_000) + setMaxConnTotal(2_000) + } + + // Customize the underlying Apache client for other settings customizeClient { // this: HttpAsyncClientBuilder setProxy(HttpHost("127.0.0.1", 8080)) // ... } + + // Customize per-request settings customizeRequest { // this: RequestConfig.Builder } diff --git a/codeSnippets/snippets/_misc_client/InstallOrReplacePlugin.kt b/codeSnippets/snippets/_misc_client/InstallOrReplacePlugin.kt new file mode 100644 index 000000000..2729b4192 --- /dev/null +++ b/codeSnippets/snippets/_misc_client/InstallOrReplacePlugin.kt @@ -0,0 +1,8 @@ +import io.ktor.client.* +import io.ktor.client.engine.cio.* + +val client = HttpClient(CIO) { + installOrReplace(ContentNegotiation) { + // ... + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/_misc_client/OkHttpConfig.kt b/codeSnippets/snippets/_misc_client/OkHttpConfig.kt index 5aac11ba5..d7f2182b6 100644 --- a/codeSnippets/snippets/_misc_client/OkHttpConfig.kt +++ b/codeSnippets/snippets/_misc_client/OkHttpConfig.kt @@ -13,5 +13,6 @@ val client = HttpClient(OkHttp) { addNetworkInterceptor(interceptor) preconfigured = okHttpClientInstance + duplexStreamingEnabled = true // Only available for HTTP/2 connections } } \ No newline at end of file diff --git a/codeSnippets/snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt b/codeSnippets/snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt index 41fea0c80..2bfce3de1 100644 --- a/codeSnippets/snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt +++ b/codeSnippets/snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt @@ -54,6 +54,13 @@ fun Application.main(httpClient: HttpClient = applicationHttpClient) { } ) } + fallback = { cause -> + if (cause is OAuth2RedirectError) { + respondRedirect("/login-after-fallback") + } else { + respond(HttpStatusCode.Forbidden, cause.message) + } + } client = httpClient } } @@ -101,6 +108,9 @@ fun Application.main(httpClient: HttpClient = applicationHttpClient) { call.respondText("Hello, ${userInfo.name}!") } } + get("/login-after-fallback") { + call.respondText("Redirected after fallback") + } } } diff --git a/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts b/codeSnippets/snippets/aws-elastic-beanstalk/build.gradle.kts index 571554116..bf00afd5c 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts b/codeSnippets/snippets/deployment-ktor-plugin/build.gradle.kts index 9371e7d03..0b055aa6f 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.3.3" + id("io.ktor.plugin") version "3.4.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 3a100c8c2..dad4663d4 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/codeSnippets/snippets/forwarded-header/build.gradle.kts b/codeSnippets/snippets/forwarded-header/build.gradle.kts index f4dc99d61..453f145e6 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.3.3" + id("io.ktor.plugin") version "3.4.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 bb7469423..1fc3a61f3 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.20" kotlinx-coroutines = "1.10.2" kotlinxSerializationJson = "1.8.1" -ktor = "3.3.3" +ktor = "3.4.0" logback = "1.5.18" [libraries] diff --git a/codeSnippets/snippets/html/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/html/src/main/kotlin/com/example/Application.kt index 24b64b91d..2350b8d3b 100644 --- a/codeSnippets/snippets/html/src/main/kotlin/com/example/Application.kt +++ b/codeSnippets/snippets/html/src/main/kotlin/com/example/Application.kt @@ -25,5 +25,12 @@ fun Application.module() { } } } + get("/fragment") { + call.respondHtmlFragment(HttpStatusCode.Created) { + div("fragment") { + span { +"Created!" } + } + } + } } } diff --git a/codeSnippets/snippets/htmx-integration/build.gradle.kts b/codeSnippets/snippets/htmx-integration/build.gradle.kts index 724a3aebb..3877b7617 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.3.3" + id("io.ktor.plugin") version "3.4.0" } group = "com.example" diff --git a/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts b/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts index 5154c06fa..1624e2a5b 100644 --- a/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts +++ b/codeSnippets/snippets/json-kotlinx-openapi/build.gradle.kts @@ -1,5 +1,5 @@ val ktor_version: String by project -val kotlin_version: String by project +val kotlin_version = "2.2.20" val logback_version: String by project val swagger_codegen_version: String by project @@ -28,6 +28,7 @@ dependencies { implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") + implementation("io.ktor:ktor-server-routing-openapi:$ktor_version") implementation("io.swagger.codegen.v3:swagger-codegen-generators:$swagger_codegen_version") implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") diff --git a/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts b/codeSnippets/snippets/legacy-interactive-website/build.gradle.kts index e6a0ef292..7186c5812 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/codeSnippets/snippets/migrating-express-ktor/gradle.properties b/codeSnippets/snippets/migrating-express-ktor/gradle.properties index e85e89795..f349f16d4 100644 --- a/codeSnippets/snippets/migrating-express-ktor/gradle.properties +++ b/codeSnippets/snippets/migrating-express-ktor/gradle.properties @@ -1,4 +1,4 @@ -ktor_version=3.3.3 +ktor_version=3.4.0 kotlin_version=2.2.20 logback_version=1.5.6 kotlin.code.style=official diff --git a/codeSnippets/snippets/openapi-spec-gen/README.md b/codeSnippets/snippets/openapi-spec-gen/README.md new file mode 100644 index 000000000..fa096e443 --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/README.md @@ -0,0 +1,20 @@ +# OpenAPI documentation + +A sample Ktor project showing how to build OpenAPI documentation using routing annotations and the compiler +extension of the Ktor Gradle plugin. + +> This sample is a part of the [`codeSnippets`](../../README.md) Gradle project. + +## Run the application + +To run the application, execute the following command in the repository's root directory: + +```bash +./gradlew :openapi-spec-gen:run +``` + +To view the OpenAPI documentation, navigate to the following URLs: + +- [http://0.0.0.0:8080/docs.json](http://0.0.0.0:8080/docs.json) to view a JSON document of the API spec. +- [http://0.0.0.0:8080/openApi](http://0.0.0.0:8080/openApi) to view the OpenAPI UI for the API spec. +- [http://0.0.0.0:8080/swaggerUI](http://0.0.0.0:8080/swaggerUI) to view the Swagger UI for the API spec. diff --git a/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts b/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts new file mode 100644 index 000000000..b84cd429c --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts @@ -0,0 +1,40 @@ +val ktor_version: String by project +val kotlin_version = "2.2.20" +val logback_version: String by project + +plugins { + application + kotlin("jvm") + id("io.ktor.plugin") version "3.4.0" +} + +application { + mainClass = "io.ktor.server.netty.EngineMain" +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap") +} + +ktor { + openApi { + enabled = true + codeInferenceEnabled = true + onlyCommented = false + } +} + + +dependencies { + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-routing-openapi:$ktor_version") + implementation("io.ktor:ktor-server-openapi:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation:${ktor_version}") + implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}") + implementation("io.ktor:ktor-server-swagger:${ktor_version}") + implementation("io.ktor:ktor-server-netty:$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/openapi-spec-gen/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt new file mode 100644 index 000000000..3120848de --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,164 @@ +package com.example + +import io.ktor.server.routing.openapi.OpenApiDocSource +import io.ktor.server.routing.openapi.describe +import io.ktor.http.* +import io.ktor.openapi.OpenApiDoc +import io.ktor.openapi.OpenApiInfo +import io.ktor.openapi.jsonSchema +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.openapi.* +import io.ktor.server.plugins.swagger.swaggerUI +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.routing.openapi.plus +import io.ktor.utils.io.ExperimentalKtorApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module() { + install(ContentNegotiation) { + json(Json { + encodeDefaults = false + }) + } + @OptIn(ExperimentalKtorApi::class) + routing { + // Main page for marketing + get("/") { + call.respondText("

Hello, World

", ContentType.Text.Html) + } + + /** + * API endpoints for users. + * + * These will appear in the resulting OpenAPI document. + */ + val apiRoute = userCrud() + + get("/docs.json") { + val doc = OpenApiDoc(info = OpenApiInfo("My API", "1.0")) + apiRoute.descendants() + call.respond(doc) + } + + /** + * View the generated UI for the API spec. + */ + openAPI("/openApi"){ + outputPath = "docs/routes" + // title, version, etc. + info = OpenApiInfo("My API from routes", "1.0.0") + // which routes to read from to build the model + // by default, it checks for `openapi/documentation.yaml` then use the routing root as a fallback + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + apiRoute.descendants() + } + } + + /** + * View the Swagger flavor of the UI for the API spec. + */ + swaggerUI("/swaggerUI") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + apiRoute.descendants() + } + } + } +} + +fun Routing.userCrud(): Route = + route("/api") { + route("/users") { + val list = mutableListOf() + + /** + * Get a single user by ID. + * + * Path: id [ULong] the ID of the user + * + * Responses: + * – 400 The ID parameter is malformatted or missing. + * – 404 The user for the given ID does not exist. + * – 200 [User] The user found with the given ID. + */ + get("/{id}") { + val id = call.parameters["id"]?.toULongOrNull() + ?: return@get call.respond(HttpStatusCode.BadRequest) + val user = list.find { it.id == id } + ?: return@get call.respond(HttpStatusCode.NotFound) + call.respond(user) + } + + /** + * Get a list of users. + * + * – Response: 200 The list of items. + */ + @OptIn(ExperimentalKtorApi::class) + get("/users") { + val query = call.parameters["q"] + val result = if (query != null) { + list.filter {it.name.contains(query, ignoreCase = true) } + } else { + list + } + + call.respond(result) + }.describe { + summary = "Get users" + description = "Retrieves a list of users." + parameters { + query("q") { + description = "An encoded query" + required = false + } + } + responses { + HttpStatusCode.OK { + description = "A list of users" + schema = jsonSchema>() + } + HttpStatusCode.BadRequest { + description = "Invalid query" + ContentType.Text.Plain() + } + } + } + + /** + * Save a new user. + * + * – Response: 204 The new user was saved. + */ + post { + list += call.receive() + call.respond(HttpStatusCode.NoContent) + } + + /** + * Delete the user with the given ID. + * + * – Path id [ULong] the ID of the user to remove + * – Response: 400 The ID parameter is malformatted or missing. + * – Response: 404 The user for the given ID does not exist. + * – Response: 204 The user was deleted. + */ + delete("/{id}") { + val id = call.parameters["id"]?.toULongOrNull() + ?: return@delete call.respond(HttpStatusCode.BadRequest) + if (!list.removeIf { it.id == id }) + return@delete call.respond(HttpStatusCode.NotFound) + call.respond(HttpStatusCode.NoContent) + } + + } +} + +@Serializable +data class User(val id: ULong, val name: String) diff --git a/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf new file mode 100644 index 000000000..2d8cb23be --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf @@ -0,0 +1,8 @@ +ktor { + deployment { + port = 8080 + } + application { + modules = [ com.example.ApplicationKt.module ] + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml new file mode 100644 index 000000000..05f2549ee --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt b/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt new file mode 100644 index 000000000..2236d246b --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt @@ -0,0 +1,14 @@ +package com.example + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.testing.* +import kotlin.test.* + +class ApplicationTest { + @Test + fun testRoot() = testApplication { + } +} diff --git a/codeSnippets/snippets/opentelemetry/gradle/libs.versions.toml b/codeSnippets/snippets/opentelemetry/gradle/libs.versions.toml index c72b600b6..5f3c3dafd 100644 --- a/codeSnippets/snippets/opentelemetry/gradle/libs.versions.toml +++ b/codeSnippets/snippets/opentelemetry/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.1.10" -ktor = "3.3.3" +ktor = "3.4.0" logback = "1.4.14" opentelemetry = "2.18.1-alpha" opentelemetry_semconv = "1.34.0" diff --git a/codeSnippets/snippets/proguard/build.gradle.kts b/codeSnippets/snippets/proguard/build.gradle.kts index 0a4b69955..c859da1a4 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/codeSnippets/snippets/server-http-request-lifecycle/README.md b/codeSnippets/snippets/server-http-request-lifecycle/README.md new file mode 100644 index 000000000..7e5a5a2c2 --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/README.md @@ -0,0 +1,14 @@ +# HTTP request lifecycle + +A sample Ktor project showing how to cancel request processing as soon as the client disconnects, using the +`HttpRequestLifecycle` plugin. + +> This sample is a part of the [`codeSnippets`](../../README.md) Gradle project. + +## Running + +To run the sample, execute the following command in the repository's root directory: + +```bash +./gradlew :server-http-request-lifecycle:run +``` diff --git a/codeSnippets/snippets/server-http-request-lifecycle/build.gradle.kts b/codeSnippets/snippets/server-http-request-lifecycle/build.gradle.kts new file mode 100644 index 000000000..2f53a9848 --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/build.gradle.kts @@ -0,0 +1,30 @@ +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project + +plugins { + application + kotlin("jvm") + kotlin("plugin.serialization").version("2.2.20") +} + +application { + mainClass.set("io.ktor.server.netty.EngineMain") +} + +repositories { + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") + testImplementation("io.ktor:ktor-server-netty") + testImplementation("io.ktor:ktor-client-cio") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") + testImplementation(kotlin("test")) +} diff --git a/codeSnippets/snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt new file mode 100644 index 000000000..aa026eb57 --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,37 @@ +package com.example + +/* + Important: The contents of this file are referenced by line numbers in `server-http-request-lifecycle.md`. + If you add, remove, or modify any lines, ensure you update the corresponding + line numbers in the code-block element of the referenced file. +*/ + +import io.ktor.server.application.* +import io.ktor.server.http.HttpRequestLifecycle +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.utils.io.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module() { + install(HttpRequestLifecycle) { + cancelCallOnClose = true + } + + routing { + get("/long-process") { + try { + while (isActive) { + delay(10_000) + log.info("Very important work.") + } + call.respond("Completed") + } catch (e: CancellationException) { + log.info("Cleaning up resources.") + } + } + } +} diff --git a/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/application.conf b/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/application.conf new file mode 100644 index 000000000..2d8cb23be --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/application.conf @@ -0,0 +1,8 @@ +ktor { + deployment { + port = 8080 + } + application { + modules = [ com.example.ApplicationKt.module ] + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/logback.xml b/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/logback.xml new file mode 100644 index 000000000..05f2549ee --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt b/codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt new file mode 100644 index 000000000..b135344ac --- /dev/null +++ b/codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt @@ -0,0 +1,33 @@ +package com.example + +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + + +class ApplicationTest { + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testClientDisconnectionCancelsRequest() = runTest { + val server = embeddedServer(Netty, port = 8080) { + module() + }.start() + + val client = HttpClient(CIO) + + val job = launch { + client.get("http://localhost:8080/long") + } + + delay(300) + job.cancelAndJoin() // Simulate client disconnect + + server.stop() + } +} diff --git a/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts b/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts index c15629aa2..5453e55a9 100644 --- a/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts +++ b/codeSnippets/snippets/server-websockets-sharedflow/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.3.3" + id("io.ktor.plugin") version "3.4.0" kotlin("plugin.serialization").version("2.2.20") } diff --git a/codeSnippets/snippets/ssl-engine-main/src/main/resources/_application.yaml b/codeSnippets/snippets/ssl-engine-main/src/main/resources/_application.yaml index 2e4522121..79e7b2d5f 100644 --- a/codeSnippets/snippets/ssl-engine-main/src/main/resources/_application.yaml +++ b/codeSnippets/snippets/ssl-engine-main/src/main/resources/_application.yaml @@ -11,4 +11,7 @@ ktor: keyStore: keystore.jks keyAlias: sampleAlias keyStorePassword: foobar - privateKeyPassword: foobar \ No newline at end of file + privateKeyPassword: foobar + trustStore: truststore.jks + trustStorePassword: foobar + enabledProtocols: ["TLSv1.2", "TLSv1.3"] \ No newline at end of file diff --git a/codeSnippets/snippets/ssl-engine-main/src/main/resources/application.conf b/codeSnippets/snippets/ssl-engine-main/src/main/resources/application.conf index 06b013c3d..09fb320a0 100644 --- a/codeSnippets/snippets/ssl-engine-main/src/main/resources/application.conf +++ b/codeSnippets/snippets/ssl-engine-main/src/main/resources/application.conf @@ -13,6 +13,9 @@ ktor { keyAlias = sampleAlias keyStorePassword = foobar privateKeyPassword = foobar + trustStore = truststore.jks + trustStorePassword = foobar + enabledProtocols = ["TLSv1.2", "TLSv1.3"] } } } \ No newline at end of file diff --git a/codeSnippets/snippets/tutorial-client-kmp/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-client-kmp/gradle/libs.versions.toml index bb0f1e0fc..b1c184678 100644 --- a/codeSnippets/snippets/tutorial-client-kmp/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-client-kmp/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidx-testExt = "1.3.0" composeMultiplatform = "1.9.0" junit = "4.13.2" kotlin = "2.2.20" -ktor = "3.3.3" +ktor = "3.4.0" kotlinx-coroutines = "1.10.2" [libraries] diff --git a/codeSnippets/snippets/tutorial-kotlin-rpc-app/build.gradle.kts b/codeSnippets/snippets/tutorial-kotlin-rpc-app/build.gradle.kts index 74076a69e..b06104489 100644 --- a/codeSnippets/snippets/tutorial-kotlin-rpc-app/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-kotlin-rpc-app/build.gradle.kts @@ -1,7 +1,7 @@ plugins { kotlin("jvm") version "2.2.21" kotlin("plugin.serialization") version "2.2.21" - id("io.ktor.plugin") version "3.3.3" + id("io.ktor.plugin") version "3.4.0" id("org.jetbrains.kotlinx.rpc.plugin") version "0.10.1" } 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 2e7fe41aa..3dd66c0b7 100644 --- a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml @@ -3,7 +3,7 @@ exposed-version = "0.56.0" h2-version = "2.3.232" kotlin-version = "2.2.20" -ktor-version = "3.3.3" +ktor-version = "3.4.0" logback-version = "1.5.18" postgres-version = "42.7.4" @@ -23,7 +23,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" } ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin-version" } -ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation-jvm", version.ref = "ktor-version" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-version" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } diff --git a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts index 2c26eb7c2..df61259c7 100644 --- a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts @@ -8,7 +8,7 @@ val h2_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.3.3" + id("io.ktor.plugin") version "3.4.0" id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } @@ -39,6 +39,6 @@ dependencies { implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-server-config-yaml:$ktor_version") testImplementation("io.ktor:ktor-server-test-host-jvm") - testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") } diff --git a/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml b/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml index ef971ca17..c45fd6b04 100644 --- a/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml +++ b/codeSnippets/snippets/tutorial-server-get-started-maven/pom.xml @@ -10,7 +10,7 @@ official 2.2.20 - 3.3.3 + 3.4.0 1.4.14 2.0.9 UTF-8 diff --git a/codeSnippets/snippets/tutorial-server-get-started/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-server-get-started/gradle/libs.versions.toml index aa9360aa4..723bd281f 100644 --- a/codeSnippets/snippets/tutorial-server-get-started/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-server-get-started/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.2.20" -ktor = "3.3.3" +ktor = "3.4.0" logback = "1.4.14" [libraries] diff --git a/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts b/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts index f4359a43b..3bb5c5a07 100644 --- a/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-restful-api/build.gradle.kts @@ -5,7 +5,7 @@ val logback_version: String by project plugins { application kotlin("jvm") - id("io.ktor.plugin") version "3.3.3" + id("io.ktor.plugin") version "3.4.0" id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } 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 0189bf3d9..9c4fdb24d 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.3.3" + id("io.ktor.plugin") version "3.4.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 7ccff7169..6741843d3 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts index b06e12807..5cf4fd046 100644 --- a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts @@ -4,7 +4,7 @@ val logback_version: String by project plugins { kotlin("jvm") - id("io.ktor.plugin") version "3.3.3" + id("io.ktor.plugin") version "3.4.0" id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } @@ -32,5 +32,5 @@ dependencies { implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-test-host-jvm") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version") } diff --git a/codeSnippets/snippets/tutorial-website-static/build.gradle.kts b/codeSnippets/snippets/tutorial-website-static/build.gradle.kts index 45bb6ffe5..c8cd2fd7b 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.3.3" + id("io.ktor.plugin") version "3.4.0" } application { diff --git a/help-versions.json b/help-versions.json index e9a58e88d..5b61e9138 100644 --- a/help-versions.json +++ b/help-versions.json @@ -10,7 +10,7 @@ "isCurrent": false }, { - "version": "3.3.3", + "version": "3.4.0", "url": "/docs/", "isCurrent": true } diff --git a/ktor.tree b/ktor.tree index 5d7f71849..4c0c31140 100644 --- a/ktor.tree +++ b/ktor.tree @@ -88,6 +88,7 @@ accepts-web-file-names="request-validation.html"/> + @@ -145,6 +146,9 @@ + @@ -392,6 +396,7 @@ + Server Plugin - + 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 - - This is an experimental feature New in version 2023.3 \ No newline at end of file diff --git a/project.ihp b/project.ihp index 7831b1c3a..5395d644a 100644 --- a/project.ihp +++ b/project.ihp @@ -14,7 +14,7 @@ diff --git a/topics/client-auth.md b/topics/client-auth.md index 763585840..9b7c06993 100644 --- a/topics/client-auth.md +++ b/topics/client-auth.md @@ -14,11 +14,11 @@ The Auth plugin handles authentication and authorization in your client applicat Ktor provides -the [Auth](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth/-auth) +the [`Auth`](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth/-auth) plugin to handle authentication and authorization in your client application. Typical usage scenarios include logging in users and gaining access to specific resources. -> On the server, Ktor provides the [Authentication](server-auth.md) plugin for handling authentication and +> On the server, Ktor provides the [`Authentication`](server-auth.md) plugin for handling authentication and > authorization. ## Supported authentication types {id="supported"} @@ -39,7 +39,7 @@ To enable authentication, you need to include the `ktor-client-auth` artifact in ## Install Auth {id="install_plugin"} -To install the `Auth` plugin, pass it to the `install` function inside a [client configuration block](client-create-and-configure.md#configure-client): +To install the `Auth` plugin, pass it to the `install()` function inside a [client configuration block](client-create-and-configure.md#configure-client): ```kotlin import io.ktor.client.* @@ -60,7 +60,9 @@ Now you can [configure](#configure_authentication) the required authentication p ### Step 1: Choose an authentication provider {id="choose-provider"} -To use a specific authentication provider ([basic](client-basic-auth.md), [digest](client-digest-auth.md), or [bearer](client-bearer-auth.md)), you need to call the corresponding function inside the `install` block. For example, to use the `basic` authentication, call the [basic](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/basic.html) function: +To use a specific authentication provider ([`basic`](client-basic-auth.md), [`digest`](client-digest-auth.md), or +[`bearer`](client-bearer-auth.md)), you need to call the corresponding function inside the `install {}` block. For example, +to use the `basic` authentication, call the [`basic {}`](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/basic.html) function: ```kotlin install(Auth) { @@ -109,3 +111,76 @@ To learn how to configure settings for a specific [provider](#supported), see a * [](client-basic-auth.md) * [](client-digest-auth.md) * [](client-bearer-auth.md) + +## Token caching and cache control {id="token-caching"} + +The Basic and Bearer authentication providers maintain an internal credential or token cache. This cache allows the +client to reuse previously loaded authentication data instead of reloading it for each request, improving performance +while still allowing full control when credentials change. + +### Accessing authentication providers + +When the authentication state needs to be updated dynamically during the client session, you can access a specific +provider using the `authProvider` extension: + +```kotlin +val provider = client.authProvider() +``` + +To retrieve all installed providers, use the `authProviders` property: + +```kotlin +val providers = client.authProviders +``` + +These utilities allow you to inspect providers or clear cached tokens programmatically. + +### Clearing cached tokens + +To clear cached credentials for a single provider, use the `clearToken()` function: + +```kotlin +val provider = client.authProvider() +provider?.clearToken() +``` + +To clear cached tokens across all authentication providers that support cache clearing, use the `clearAuthTokens()` +function: + +```kotlin +client.clearAuthTokens() +``` + +Clearing cached tokens is typically used in the following scenarios: + +- When the user logs out. +- When credentials or tokens stored by your application change. +- When you need to force providers to reload the authentication state on the next request. + +Here's an example for clearing cached tokens when the user logs out: + +```kotlin +fun logout() { + client.clearAuthTokens() + storage.deleteCredentials() +} +``` + +### Controlling caching behavior + +Both Basic and Bearer authentication providers allow you to control whether tokens or credentials are cached between +requests using the `cacheTokens` option. + +For example, you can disable caching when credentials are dynamically provided: + +```kotlin +basic { + cacheTokens = false // Reloads credentials for every request + credentials { + loadCurrentCredentials() + } +} +``` + +Disabling token caching is especially useful when authentication data changes frequently or must reflect the most +recent state. \ No newline at end of file diff --git a/topics/client-basic-auth.md b/topics/client-basic-auth.md index 2302740fa..1902c677e 100644 --- a/topics/client-basic-auth.md +++ b/topics/client-basic-auth.md @@ -16,39 +16,45 @@ The Basic [authentication scheme](client-auth.md) can be used for logging in use The basic authentication flow looks as follows: -1. A client makes a request without the `Authorization` header to a specific resource in a server application. -2. A server responds to a client with a `401` (Unauthorized) response status and uses a `WWW-Authenticate` response header to provide information that the basic authentication scheme is used to protect a route. A typical `WWW-Authenticate` header looks like this: +1. A client makes a request to a protected resource in a server application without the `Authorization` header. +2. The server responds with a `401 Unauthorized` response status and uses a `WWW-Authenticate` response header to + indicate that Basic authentication is required. A typical `WWW-Authenticate` header looks like this: ``` WWW-Authenticate: Basic realm="Access to the '/' path", charset="UTF-8" ``` {style="block"} - The Ktor client allows you to send credentials without waiting the `WWW-Authenticate` header using the `sendWithoutRequest` [function](#configure). + The Ktor client allows you to send credentials preemptively – without waiting for the `WWW-Authenticate` header – + by using the [`sendWithoutRequest()` function](#configure). -3. Usually, a client displays a login dialog where a user can enter credentials. Then, a client makes a request with the `Authorization` header containing a username and password pair encoded using Base64, for example: +3. The client typically prompts the user for credentials. It then makes a request with the `Authorization` header + containing a username and password pair encoded using Base64, for example: ``` Authorization: Basic amV0YnJhaW5zOmZvb2Jhcg ``` {style="block"} -4. A server validates credentials sent by the client and responds with the requested content. +4. The server validates the credentials sent by the client and responds with the requested content. ## Configure basic authentication {id="configure"} -To send user credentials in the `Authorization` header using the `Basic` scheme, you need to configure the `basic` authentication provider as follows: +To send user credentials in the `Authorization` header using the `Basic` scheme, you need to configure the `basic` +authentication provider: -1. Call the [basic](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/basic.html) function inside the `install` block. -2. Provide the required credentials using [BasicAuthCredentials](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/-basic-auth-credentials/index.html) and pass this object to the [credentials](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/-basic-auth-config/credentials.html) function. +1. Call the [`basic`](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/basic.html) function + inside the `install(Auth)` block. +2. Provide the required credentials using [`BasicAuthCredentials`](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/-basic-auth-credentials/index.html) and pass this object to the [`credentials`](https://api.ktor.io/ktor-client-auth/io.ktor.client.plugins.auth.providers/-basic-auth-config/credentials.html) function. 3. Configure the realm using the `realm` property. ```kotlin ``` {src="snippets/client-auth-basic/src/main/kotlin/com/example/Application.kt" include-lines="17-26"} -4. Optionally, enable sending credentials in the initial request without waiting for a `401` (Unauthorized) response with the `WWW-Authenticate` header. You need to call the `sendWithoutRequest` function returning boolean and check the request parameters. +4. (Optional) Enable preemptive authentication using the `sendWithoutRequest` function, which checks the request + parameters and decides whether to attach credentials to the initial request. ```kotlin install(Auth) { @@ -60,7 +66,21 @@ To send user credentials in the `Authorization` header using the `Basic` scheme, } } ``` - -> You can find the full example here: [client-auth-basic](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/client-auth-basic). +5. (Optional) Disable credential caching. By default, credentials returned by the `credentials {}` provider are cached + for reuse across requests. You can disable caching with the `cacheTokens` option: + + ```kotlin + basic { + cacheTokens = false // Reloads credentials for every request + // ... + } + ``` + Disabling caching is useful when credentials may change during the client session or must reflect the latest + stored state. + + > For details on clearing cached credentials programmatically, see the general [Token caching and cache control](client-auth.md#token-caching) + > section. + +> For a full example of basic authentication in Ktor Client, see [client-auth-basic](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/client-auth-basic). diff --git a/topics/client-bearer-auth.md b/topics/client-bearer-auth.md index e5390a9bc..68d2dfbdb 100644 --- a/topics/client-bearer-auth.md +++ b/topics/client-bearer-auth.md @@ -79,7 +79,7 @@ A Ktor client allows you to configure a token to be sent in the `Authorization` c. The client makes one more request to a protected resource automatically using a new token this time. -4. Optionally, specify a condition for sending credentials without waiting for the `401` (Unauthorized) response. For example, you can check whether a request is made to a specified host. +4. (Optional) Specify a condition for sending credentials without waiting for the `401` (Unauthorized) response. For example, you can check whether a request is made to a specified host. ```kotlin install(Auth) { @@ -92,6 +92,22 @@ A Ktor client allows you to configure a token to be sent in the `Authorization` } ``` +5. (Optional) Use the `cacheTokens` option to control whether bearer tokens are cached between requests. Disabling + caching forces the client to reload tokens for every request, which can be useful when tokens change frequently: + + ```kotlin + install(Auth) { + bearer { + cacheTokens = false // Reloads tokens for every request + loadTokens { + loadDynamicTokens() + } + } + } + ``` + + > For details on clearing cached credentials programmatically, see the general [Token caching and cache control](client-auth.md#token-caching) + > section. ## Example: Using Bearer authentication to access Google API {id="example-oauth-google"} diff --git a/topics/client-default-request.md b/topics/client-default-request.md index 56a067a85..7c8848bff 100644 --- a/topics/client-default-request.md +++ b/topics/client-default-request.md @@ -12,7 +12,7 @@ The DefaultRequest plugin allows you to configure default parameters for all requests. -The [DefaultRequest](https://api.ktor.io/ktor-client-core/io.ktor.client.plugins/-default-request/index.html) plugin allows you to configure default parameters for all [requests](client-requests.md): specify a base URL, add headers, configure query parameters, and so on. +The [`DefaultRequest`](https://api.ktor.io/ktor-client-core/io.ktor.client.plugins/-default-request/index.html) plugin allows you to configure default parameters for all [requests](client-requests.md): specify a base URL, add headers, configure query parameters, and so on. ## Add dependencies {id="add_dependencies"} @@ -34,7 +34,7 @@ val client = HttpClient(CIO) { } ``` -Or call the `defaultRequest` function and [configure](#configure) required request parameters: +Or call the `defaultRequest()` function and [configure](#configure) required request parameters: ```kotlin import io.ktor.client.* @@ -48,6 +48,30 @@ val client = HttpClient(CIO) { } ``` +### Replace existing configuration {id="default_request_replace"} + +If the `DefaultRequest` plugin has already been installed, you can replace its existing configuration in one of the following ways: + +- Use the `replace` parameter of the `defaultRequest()` function: + +```kotlin +val client = HttpClient(CIO) { + defaultRequest(replace = true) { + // this: DefaultRequestBuilder + } +} +``` + +- Use the generic `installOrReplace()` function: + +```kotlin +val client = HttpClient(CIO) { + installOrReplace(DefaultRequest) { + // this: DefaultRequestBuilder + } +} +``` + ## Configure DefaultRequest {id="configure"} ### Base URL {id="url"} diff --git a/topics/client-engines.md b/topics/client-engines.md index e6f3dfcdb..ecadff8c5 100644 --- a/topics/client-engines.md +++ b/topics/client-engines.md @@ -132,7 +132,12 @@ This is the recommended Apache-based engine for new projects. ```kotlin ``` - {src="snippets/_misc_client/Apache5Create.kt" include-lines="1-4,7-23"} + {src="snippets/_misc_client/Apache5Create.kt" include-lines="1-4,7-33"} + + - Use `configureConnectionManager` for connection manager settings, such as maximum connections. This preserves + Ktor-managed engine behavior. + - Use `customizeClient` only for settings unrelated to the connection manager, such as proxy, interceptors, or + logging. ### Java {id="java"} @@ -284,7 +289,7 @@ To use the `WinHttp` engine, follow the steps below: ``` 3. Configure the engine in the `engine {}` block using [ - `WinHttpClientEngineConfig`](https://api.ktor.io/ktor-client-winhttp/io.ktor.client.engine.winhttp/-winhttp-client-engine-config/index.html). + `WinHttpClientEngineConfig`](https://api.ktor.io/ktor-client-winhttp/io.ktor.client.engine.winhttp/-win-http-client-engine-config/index.html). For example, you can use the `protocolVersion` property to change the HTTP version: ```kotlin ``` @@ -311,7 +316,7 @@ For desktop platforms, Ktor provides the `Curl` engine. It is supported on `linu val client = HttpClient(Curl) ``` -3. Configure the engine in the `engine {}` block using `CurlClientEngineConfig`. +3. Configure the engine in the `engine {}` block using [`CurlClientEngineConfig`](https://api.ktor.io/ktor-client-curl/io.ktor.client.engine.curl/-curl-client-engine-config/index.html). For example, disable SSL verification for testing purposes: ```kotlin ``` diff --git a/topics/client-plugins.md b/topics/client-plugins.md index 26b0a5772..c58756a6b 100644 --- a/topics/client-plugins.md +++ b/topics/client-plugins.md @@ -1,35 +1,60 @@ [//]: # (title: Client plugins) -Get acquainted with plugins that provide common functionality, for example, logging, serialization, authorization, etc. +Learn how to use client plugins to add common functionality, such as logging, serialization, and authorization. -Many applications require common functionality that is out of scope of the application logic. This could be things like [logging](client-logging.md), [serialization](client-serialization.md), or [authorization](client-auth.md). All of these are provided in Ktor by means of what we call **Plugins**. +Many applications require common functionality that is not part of the core application logic, such as +[logging](client-logging.md), [serialization](client-serialization.md), or [authorization](client-auth.md). In Ktor, +this functionality is provided by client _plugins_. +## Add plugin dependencies {id="plugin-dependency"} -## Add plugin dependency {id="plugin-dependency"} -A plugin might require a separate [dependency](client-dependencies.md). For example, the [Logging](client-logging.md) plugin requires adding the `ktor-client-logging` artifact in the build script: +Some plugins require an additional [dependency](client-dependencies.md). For example, to use the [Logging](client-logging.md) plugin, you need to add the +`ktor-client-logging` artifact in your build script: -You can learn which dependencies you need from a topic for a required plugin. - +Each plugin’s documentation specifies any required dependencies. ## Install a plugin {id="install"} -To install a plugin, you need to pass it to the `install` function inside a [client configuration block](client-create-and-configure.md#configure-client). For example, installing the `Logging` plugin looks as follows: + +To install a plugin, pass it to the `install()` function inside a [client configuration block](client-create-and-configure.md#configure-client). + +For example, installing the `Logging` plugin looks as follows: ```kotlin ``` -{src="snippets/_misc_client/InstallLoggingPlugin.kt"} +{src="snippets/_misc_client/InstallLoggingPlugin.kt" include-lines="1-7"} + +### Install or replace a plugin {id="install_or_replace"} +In some cases, a plugin may already be installed — for example, by shared client configuration code. In such cases, you +can replace its configuration using the `installOrReplace()` function: + +```kotlin +``` +{src="snippets/_misc_client/InstallOrReplacePlugin.kt" include-symbol="client"} + +This function installs the plugin if it is not present or replaces its existing configuration if it has already been +installed. ## Configure a plugin {id="configure_plugin"} -You can configure a plugin inside the `install` block. For example, for the [Logging](client-logging.md) plugin, you can specify the logger, logging level, and condition for filtering log messages: + +Most plugins expose configuration options that can be set inside the `install` block. + +For example, the [`Logging`](client-logging.md) plugin allows you to specify the logger, logging level, and condition for filtering log +messages: + ```kotlin ``` -{src="snippets/client-logging/src/main/kotlin/com/example/Application.kt" include-lines="12-20"} +{src="snippets/client-logging/src/main/kotlin/com/example/Application.kt" include-symbol="client"} ## Create a custom plugin {id="custom"} -To learn how to create custom plugins, refer to [](client-custom-plugins.md). + +If the existing plugins do not meet your needs, you can create your own custom client plugins. Custom plugins allow you +to intercept requests and responses and implement reusable behavior. + +To learn more, see [](client-custom-plugins.md). diff --git a/topics/client-responses.md b/topics/client-responses.md index 3a4ecf129..d64f5fc28 100644 --- a/topics/client-responses.md +++ b/topics/client-responses.md @@ -1,6 +1,6 @@ [//]: # (title: Receiving responses) - + Learn how to receive responses, get a response body and obtain response parameters. @@ -39,15 +39,37 @@ property: The [ `HttpResponse.headers`](https://api.ktor.io/ktor-client-core/io.ktor.client.statement/-http-response/index.html) -property allows you to get a [Headers](https://api.ktor.io/ktor-http/io.ktor.http/-headers/index.html) map containing -all response headers. Additionally, `HttpResponse` exposes the following functions for receiving specific header values: +property allows you to get a [`Headers`](https://api.ktor.io/ktor-http/io.ktor.http/-headers/index.html) map containing +all response headers. -* `contentType` for the `Content-Type` header value -* `charset` for a charset from the `Content-Type` header value. -* `etag` for the `E-Tag` header value. -* `setCookie` for the `Set-Cookie` header value. - > Ktor also provides the [HttpCookies](client-cookies.md) plugin that allows you to keep cookies between calls. +Additionally, the `HttpResponse` class exposes the following functions for receiving specific header values: +* `contentType()` for the `Content-Type` header value. +* `charset()` for a charset from the `Content-Type` header value. +* `etag()` for the `E-Tag` header value. +* `setCookie()` for the `Set-Cookie` header value. + > Ktor also provides the [`HttpCookies`](client-cookies.md) plugin that allows you to keep cookies between calls. + + +#### Split header values + +If a header can contain multiple comma — or semicolon — separated values, you can use the `.getSplitValues()` function +to retrieve all split values from a header: + +```kotlin +val httpResponse: HttpResponse = client.get("https://ktor.io/") +val headers: Headers = httpResponse.headers + +val splitValues = headers.getSplitValues("X-Multi-Header")!! +// ["1", "2", "3"] +``` + +Using the usual `get` operator returns values without splitting: + +```kotlin +val values = headers["X-Multi-Header"]!! +// ["1, 2", "3"] +``` ## Receive response body {id="body"} @@ -139,29 +161,41 @@ Once the form processing is complete, each part is disposed of using the `.dispo ### Streaming data {id="streaming"} -When you call the `HttpResponse.body` function to get a body, Ktor processes a response in memory and returns a full -response body. If you need to get chunks of a response sequentially instead of waiting for the entire response, use -`HttpStatement` with -scoped [execute](https://api.ktor.io/ktor-client-core/io.ktor.client.statement/-http-statement/execute.html) +By default, calling `HttpResponse.body()` loads the full response into memory. For large responses or file downloads, +it’s often better to process data in chunks without waiting for the full body. + +Ktor provides several ways to do this using [`ByteReadChannel`](https://api.ktor.io/ktor-io/io.ktor.utils.io/-byte-read-channel/index.html) +and I/O utilities. + +#### Sequential chunk processing + +To process the response sequentially in chunks, use `HttpStatement` with +a scoped [`execute`](https://api.ktor.io/ktor-client-core/io.ktor.client.statement/-http-statement/execute.html) block. -A [runnable example](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/client-download-streaming) -below shows how to receive a response content in chunks (byte packets) and save them in a file: + +The following example demonstrates reading a response in chunks and saving it to a file: ```kotlin ``` {src="snippets/client-download-streaming/src/main/kotlin/com/example/Application.kt" include-lines="15-37"} -> For converting between Ktor channels and types like `RawSink`, `RawSource`, or `OutputStream`, see -> [I/O interoperability](io-interoperability.md). -> -{style="tip"} - -In this example, [`ByteReadChannel`](https://api.ktor.io/ktor-io/io.ktor.utils.io/-byte-read-channel/index.html) is used -to read data asynchronously. Using `ByteReadChannel.readRemaining()` retrieves all available bytes in the channel, while +Using `ByteReadChannel.readRemaining()` retrieves all available bytes in the channel, while `Source.transferTo()` directly writes the data to the file, reducing unnecessary allocations. -To save a response body to a file without extra processing, you can use the -[`ByteReadChannel.copyAndClose()`](https://api.ktor.io/ktor-io/io.ktor.utils.io/copy-and-close.html) function instead: +> For the full streaming example, see +> [client-download-streaming](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/client-download-streaming). + +#### Writing the response directly to a file + +For simple downloads where chunk-by-chunk processing is not needed, you can choose one of the following approaches: + +- [Copy all bytes to a `ByteWriteChannel` and close](#copyAndClose). +- [Copy to a `RawSink`](#readTo). + +##### Copy all bytes to a `ByteWriteChannel` and close {id="copyAndClose"} + +The [`ByteReadChannel.copyAndClose()`](https://api.ktor.io/ktor-io/io.ktor.utils.io/copy-and-close.html) function +copies all remaining bytes from a `ByteReadChannel` to a `ByteWriteChannel` and then closes both channels automatically: ```Kotlin client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> @@ -170,3 +204,31 @@ client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse println("A file saved to ${file.path}") } ``` + +This is convenient for full file downloads where you don’t need to manually manage channels. + +##### Copy to a `RawSink` {id="readTo"} + +The [`ByteReadChannel.readTo()`](https://api.ktor.io/ktor-io/io.ktor.utils.io/read-to.html) +function writes bytes directly to a `RawSink` without intermediate buffers: + +```kotlin +val file = File.createTempFile("files", "index") +val stream = file.outputStream().asSink() + +client.prepareGet(url).execute { httpResponse -> + val channel: ByteReadChannel = httpResponse.body() + channel.readTo(stream) +} +println("A file saved to ${file.path}") + +``` + +Unlike `.copyAndClose()`, the sink remains open after writing and it is only closed automatically if an error occurs +during the transfer. + + +> For converting between Ktor channels and types like `RawSink`, `RawSource`, or `OutputStream`, see +> [I/O interoperability](io-interoperability.md). +> +{style="tip"} \ No newline at end of file diff --git a/topics/client-webrtc.md b/topics/client-webrtc.md index 349e36e3c..ecdb105b4 100644 --- a/topics/client-webrtc.md +++ b/topics/client-webrtc.md @@ -40,8 +40,10 @@ To use `WebRtcClient`, you need to include the `%artifact_name%` artifact in the 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. +- JS/Wasm: `JsWebRtc` – uses [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API), +[Media Capture and Streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API) browser APIs. +- Android: `AndroidWebRtc` – uses a pre-compiled WebRTC library for Android by [Stream](https://github.com/GetStream/webrtc-android) and Android media APIs. +- iOS: `IosWebRtc` - uses [WebRTC SDK](https://github.com/webrtc-sdk) for iOS and native [AVFoundation](https://developer.apple.com/documentation/avfoundation) framework. 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): @@ -69,6 +71,16 @@ val androidClient = WebRtcClient(AndroidWebRtc) { } ``` + + + + +```kotlin +val iosClient = WebRtcClient(IosWebRtc) { + // the same configuration, no extra context needed +} +``` + @@ -221,15 +233,115 @@ scope.launch { } ``` +## Platform-specific logic + +This API provides high-level abstractions, but there are use-cases that may require accessing platform-specific APIs. +You can use the `.getNative()` extension functions to retrieve the underlying implementations. +Platform-specific libraries are exposed as transitive libraries, except for `WebRTC-SDK` CocoaPod on iOS. + + + + +```kotlin +// DOM API is imported from `kotlin-wrappers` + +val videoTrack = rtcClient.createVideoTrack() +val jsStream = MediaStream().apply { + val nativeTrack: MediaStreamTrack = videoTrack.getNative() + addTrack(nativeTrack) +} + +// start rendering video +val videoElement = document.createElement("video") as HTMLVideoElement +videoElement.srcObject = jsStream +videoElement.autoplay = true + +// stop rendering video +videoElement.srcObject = null +``` + + + + +```kotlin +val eglBase = org.webrtc.EglBase.create() // should be unique in the app + +val videoTrack = rtcClient.createVideoTrack() +val nativeTrack: org.webrtc.VideoTrack = videoTrack.getNative() + +// create a surface to render incoming video frames +val renderer = org.webrtc.SurfaceViewRenderer() +renderer.init(eglBase.eglBaseContext, null) + +// start rendering video +videoTrack.addSink(renderer) + +// stop rendering video +videoTrack.removeSink(renderer) +renderer.release() +``` + + + + + +```kotlin +val videoTrack = rtcClient.createVideoTrack() +val nativeTrack: RTCVideoTrack = videoTrack.getNative() + +// create a surface to render incoming video frames +val videoView = RTCMTLVideoView() // iOS UIKit View + +// start rendering video +nativeTrack.addRenderer(videoView) + +// stop rendering video +nativeTrack.removeRenderer(videoView) +``` + +To use the `WebRTC-SDK` API, you need to install it manually: + +```kotlin +// build.gradle.kts +kotlin { + cocoapods { + pod("WebRTC-SDK") { + version = "137.7151.04" // or newer + // Default module name is `WebRTC-SDK`, you can change it for convenience + moduleName = "WebRTC" + packageName = "WebRTC" + } + } +} +``` + + + + +```kotlin +// On Android and iOS, audio track playback can be started/stopped without using `getNative()` +// In browser, you still should create an