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/gradle.properties b/codeSnippets/gradle.properties index fbac9b95e..dc49476a6 100644 --- a/codeSnippets/gradle.properties +++ b/codeSnippets/gradle.properties @@ -6,7 +6,7 @@ kotlin.native.binary.memoryModel = experimental org.gradle.configureondemand = false # versions kotlin_version = 2.2.20 -ktor_version = 3.3.3 +ktor_version = 3.4.0-eap-1477 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..fc1e6f2e7 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -161,6 +161,7 @@ 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") 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/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/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/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/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 @@ + 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..1227efc39 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,33 @@ 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"} + +[//]: # (TODO: Add API link) + +The [`ByteReadChannel.readTo()`]() +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/server-api-key-auth.md b/topics/server-api-key-auth.md new file mode 100644 index 000000000..edaed06c2 --- /dev/null +++ b/topics/server-api-key-auth.md @@ -0,0 +1,285 @@ +[//]: # (title: API Key authentication) + + + + + + +

+Required dependencies: io.ktor:ktor-server-auth, io.ktor:%artifact_name% +

+ +
+ +API Key authentication is a simple authentication method where clients pass a secret key as part of their request, +typically in a header. This key serves as both the identifier and the authentication mechanism. + +Ktor allows you to use API Key authentication for securing [routes](server-routing.md) and validating client requests. + +> You can get general information about authentication in Ktor in the [](server-auth.md) section. + +> API Keys should be kept secret and transmitted securely. It's recommended to use [HTTPS/TLS](server-ssl.md) to protect +> API keys in transit. +> +{style="note"} + +## Add dependencies {id="add_dependencies"} + +To enable API Key authentication, add the `ktor-server-auth` and `%artifact_name%` artifacts in the build script: + + + + +```kotlin +implementation("io.ktor:%artifact_name%:$ktor_version") +implementation("io.ktor:ktor-server-auth:$ktor_version") +``` + + + +```Groovy +implementation "io.ktor:%artifact_name%:$ktor_version" +implementation "io.ktor:ktor-server-auth:$ktor_version" + +``` + + + + +```xml + + io.ktor + %artifact_name%-jvm + ${ktor_version} + + + io.ktor + ktor-server-auth + ${ktor_version} + +``` + + + + +## API Key authentication flow {id="flow"} + +The API Key authentication flow looks as follows: + +1. A client makes a request with an API key included in a header (typically `X-API-Key`) to a + specific [route](server-routing.md) in a server application. +2. The server validates the API key using custom validation logic. +3. If the key is valid, the server responds with the requested content. If the key is invalid or missing, the server + responds with a `401 Unauthorized` status. + +## Install API Key authentication {id="install"} + +To install the `apiKey` authentication provider, call +the [`apiKey`](https://api.ktor.io/ktor-server-auth/io.ktor.server.auth/api-key.html) function inside the +`install(Authentication)` block: + +```kotlin +import io.ktor.server.application.* +import io.ktor.server.auth.* +// ... +install(Authentication) { + apiKey { + // Configure API Key authentication + } +} +``` + +You can optionally specify a [provider name](server-auth.md#provider-name) that can be used +to [authenticate a specified route](#authenticate-route). + +## Configure API Key authentication {id="configure"} + +In this section, we'll see the configuration specifics of the `apiKey` authentication provider. + +> To learn how to configure different authentication providers in Ktor, see [](server-auth.md#configure). + +### Step 1: Configure an API Key provider {id="configure-provider"} + +The `apiKey` authentication provider exposes its settings via +the [`ApiKeyAuthenticationProvider.Config`](https://api.ktor.io/ktor-server-auth/io.ktor.server.auth/-api-key-authentication-provider/-config/index.html) +class. In the example below, the following settings are specified: + +* The `validate` function receives the API key extracted from the request and returns a `Principal` in the case of + successful authentication or `null` if authentication fails. + +Here's a minimal example: + +```kotlin +data class AppPrincipal(val key: String) : Principal + +install(Authentication) { + apiKey { + validate { keyFromHeader -> + val expectedApiKey = "this-is-expected-key" + keyFromHeader + .takeIf { it == expectedApiKey } + ?.let { AppPrincipal(it) } + } + } +} +``` + +#### Customize key location {id="key-location"} + +By default, the `apiKey` provider looks for the API key in the `X-API-Key` header. + +You can use `headerName` to specify a custom header: + +```kotlin +apiKey("api-key-header") { + headerName = "X-Secret-Key" + validate { key -> + // ... + } +} +``` + +### Step 2: Validate API keys {id="validate"} + +The validation logic depends on your application's requirements. Here are common approaches: + +#### Static key comparison {id="static-key"} + +For simple cases, you can compare against a predefined key: + +```kotlin +apiKey { + validate { keyFromHeader -> + val expectedApiKey = environment.config.property("api.key").getString() + keyFromHeader + .takeIf { it == expectedApiKey } + ?.let { AppPrincipal(it) } + } +} +``` + +> Store sensitive API keys in configuration files or environment variables, not in source code. +> +{style="note"} + +#### Database lookup {id="database-lookup"} + +For multiple API keys, validate against a database: + +```kotlin +apiKey { + validate { keyFromHeader -> + // Looks up the key in the database + val user = database.findUserByApiKey(keyFromHeader) + user?.let { UserIdPrincipal(it.username) } + } +} +``` + +#### Multiple validation criteria {id="multiple-criteria"} + +You can implement complex validation logic: + +```kotlin +apiKey { + validate { keyFromHeader -> + val apiKey = database.findApiKey(keyFromHeader) + + // Checks if the key exists, is active, and not expired + if (apiKey != null && + apiKey.isActive && + apiKey.expiresAt > Clock.System.now() + ) { + UserIdPrincipal(apiKey.userId) + } else { + null + } + } +} +``` + +### Step 3: Configure challenge {id="challenge"} + +You can customize the response sent when authentication fails using the `challenge` function: + +```kotlin +apiKey { + validate { key -> + // Validation logic + } + challenge { defaultScheme, realm -> + call.respond( + HttpStatusCode.Unauthorized, + "Invalid or missing API key" + ) + } +} +``` + +### Step 4: Protect specific resources {id="authenticate-route"} + +After configuring the `apiKey` provider, you can protect specific resources in your application using the +[`authenticate`](server-auth.md#authenticate-route) function. In the case of successful authentication, you can +retrieve an authenticated principal inside a route handler using the `call.principal` function. + +```kotlin +routing { + authenticate { + get("/") { + val principal = call.principal()!! + call.respondText("Hello, authenticated client! Your key: ${principal.key}") + } + } +} +``` + +## API Key authentication example {id="complete-example"} + +Here's a complete minimal example of API Key authentication: + +```kotlin +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +data class AppPrincipal(val key: String) : Principal + +fun Application.module() { + val expectedApiKey = "this-is-expected-key" + + install(Authentication) { + apiKey { + validate { keyFromHeader -> + keyFromHeader + .takeIf { it == expectedApiKey } + ?.let { AppPrincipal(it) } + } + } + } + + routing { + authenticate { + get("/") { + val principal = call.principal()!! + call.respondText("Key: ${principal.key}") + } + } + } +} +``` + +## Best practices {id="best-practices"} + +When implementing API Key authentication, consider the following best practices: + +1. **Use HTTPS**: Always transmit API keys over HTTPS to prevent interception. +2. **Store securely**: Never hardcode API keys in source code. Use environment variables or secure configuration + management. +3. **Key rotation**: Implement a mechanism for rotating API keys periodically. +4. **Rate limiting**: Combine API Key authentication with rate limiting to prevent abuse. +5. **Logging**: Log authentication failures for security monitoring, but never log the actual API keys. +6. **Key format**: Use cryptographically secure random strings for API keys (for example, UUID or base64-encoded random + bytes). +7. **Multiple keys**: Consider supporting multiple API keys per user for different applications or purposes. +8. **Expiration**: Implement key expiration for enhanced security. diff --git a/topics/server-auth.md b/topics/server-auth.md index 3e01a9d4e..44723d3f3 100644 --- a/topics/server-auth.md +++ b/topics/server-auth.md @@ -33,8 +33,9 @@ Ktor supports the following authentication and authorization schemes: HTTP provides a [general framework](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) for access control and authentication. In Ktor, you can use the following HTTP authentication schemes: * [Basic](server-basic-auth.md) - uses `Base64` encoding to provide a username and password. Generally is not recommended if not used in combination with HTTPS. * [Digest](server-digest-auth.md) - an authentication method that communicates user credentials in an encrypted form by applying a hash function to the username and password. -* [Bearer](server-bearer-auth.md) - an authentication scheme that involves security tokens called bearer tokens. +* [Bearer](server-bearer-auth.md) - an authentication scheme that involves security tokens called bearer tokens. The Bearer authentication scheme is used as part of [OAuth](server-oauth.md) or [JWT](server-jwt.md), but you can also provide custom logic for authorizing bearer tokens. +* [API Key](server-api-key-auth.md) - a simple authentication method where clients pass a secret key in a header. ### Form-based authentication {id="form-auth"} diff --git a/topics/server-compression.md b/topics/server-compression.md index 519023f0e..0886e6e30 100644 --- a/topics/server-compression.md +++ b/topics/server-compression.md @@ -16,8 +16,13 @@ -Ktor provides the capability to compress response body and decompress request body by using the [Compression](https://api.ktor.io/ktor-server-compression/io.ktor.server.plugins.compression/-compression.html) plugin. -You can use different compression algorithms, including `gzip` and `deflate`, specify the required conditions for compressing data (such as a content type or response size), or even compress data based on specific request parameters. +Ktor provides the capability to compress response body and decompress request body by using the [`Compression`](https://api.ktor.io/ktor-server-compression/io.ktor.server.plugins.compression/-compression.html) +plugin. + +With the `Compression` plugin, you can: +- Use different compression algorithms, including `gzip`, `ztsd` and `deflate`. +- Specify the required conditions for compressing data, such as a content type or response size. +- Compress data based on specific request parameters. > Note that the `%plugin_name%` plugin does not currently support `SSE` responses. > @@ -51,6 +56,7 @@ To enable only specific encoders, call the corresponding extension functions, fo install(Compression) { gzip() deflate() + ztsd() } ``` @@ -64,10 +70,13 @@ install(Compression) { deflate { priority = 1.0 } + ztsd { + priority = 0.8 + } } ``` -In the example above, `deflate` has a higher priority value and takes precedence over `gzip`. Note that the server first +In the example above, `deflate` has a higher priority value and takes precedence over `gzip` and `ztsd`. Note that the server first looks at the [quality](https://developer.mozilla.org/en-US/docs/Glossary/Quality_Values) values within the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header and then takes into account the specified priorities. @@ -94,10 +103,10 @@ value. To do this, pass the desired value (in bytes) to the `minimumSize` functi ```kotlin install(Compression) { - deflate { - minimumSize(1024) - } + deflate { + minimumSize(1024) } +} ``` @@ -132,6 +141,19 @@ install(Compression) { } ``` +## Compression Level {id="compression_level"} + +`Ztsd` comes with a configurable compression level. By default, it is set to `3`, and you can configure it using +the `compressionLevel` property. + +```kotlin +install(Compression) { + zstd { + compressionLevel = 3 + } +} +``` + ## Implement custom encoder {id="custom_encoder"} If necessary, you can provide your own encoder by implementing diff --git a/topics/server-html-dsl.md b/topics/server-html-dsl.md index 5bf6ec54f..4c12a90e6 100644 --- a/topics/server-html-dsl.md +++ b/topics/server-html-dsl.md @@ -28,7 +28,7 @@ The example below shows a sample HTML DSL and a corresponding HTML to be sent to ```kotlin ``` -{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="3-8,11-29"} +{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="3-8,11-27,35-36"} @@ -47,7 +47,7 @@ The example below shows a sample HTML DSL and a corresponding HTML to be sent to -The following [example](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/auth-form-html-dsl) shows how to respond with an HTML form used to collect [credential information](server-form-based-auth.md) from a user: +The following example shows how to respond with an HTML form used to collect [credential information](server-form-based-auth.md) from a user: @@ -74,10 +74,44 @@ The following [example](https://github.com/ktorio/ktor-documentation/tree/%ktor_ -You can learn how to receive form parameters on the server side from [](server-requests.md#form_parameters). +For the full example, see [auth-form-html-dsl](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/auth-form-html-dsl). +> To learn more about receiving form parameters on the server side, see [](server-requests.md#form_parameters). +> > To learn more about generating HTML using kotlinx.html, see the [kotlinx.html wiki](https://github.com/Kotlin/kotlinx.html/wiki). +## Send partial HTML {id="html_fragments"} + +In addition to generating full HTML documents, you can also respond with HTML fragments using the `.respondHtmlFragment()` +function. + +HTML fragments are useful when returning partial markup that does not require a full `` document, such as dynamic +updates used by libraries like HTMX. + + + + +```kotlin +``` +{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="3-8,11-12,28-36"} + + + + +```html +
+ + Created! + +
+ +``` + +
+
+ +This function works similarly to `.respondHtml()`, but it renders only the content you define inside the builder, +without adding root HTML elements. ## Templates {id="templates"} diff --git a/topics/server-http-request-lifecycle.md b/topics/server-http-request-lifecycle.md new file mode 100644 index 000000000..ee6b3b720 --- /dev/null +++ b/topics/server-http-request-lifecycle.md @@ -0,0 +1,65 @@ +[//]: # (title: HTTP request lifecycle) + + + + + + + +

+Required dependencies: io.ktor:ktor-server-core +

+ +

+Supported engines: Netty, CIO +

+ +
+ +By default, Ktor continues processing a request even if the client disconnects. The +[`%plugin_name%`](https://api.ktor.io/ktor-http/io.ktor.http/-http-request-lifecycle.html) +plugin allows you to cancel request processing as soon as the client disconnects. + +This is useful for long-running or resource-intensive requests that should stop executing when the client is no longer +waiting for a response. + +## Install and configure %plugin_name% {id="install_plugin"} + +To enable the `HttpRequestLifecycle` plugin, install it in your application module using the `install` function and +set the `cancelCallOnClose` property: + +```kotlin +``` +{src="snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt" include-lines="9-10,18-22,37"} + +When the `cancelCallOnClose` property is enabled, the `%plugin_name%` plugin installs a cancellation handler per +request. When a client disconnects, only the coroutine handling that specific route is canceled. + +Cancellation propagates through structured concurrency, so any child coroutines started from the request coroutine +(for example, using `launch` or `async`) are also canceled. A `CancellationException` is thrown at the next +suspension point. + +## Handling cancellation {id="handle_cancellation"} + +You can catch `CancellationException` to perform cleanup operations, such as releasing resources or stopping background +work: + +```kotlin +``` +{src="snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt" include-lines="19-37"} + +> Coroutine cancellation is cooperative. Blocking or CPU-bound code is not interrupted automatically. +> If request handling performs long-running work, call +> `call.coroutineContext.ensureActive()` to react to cancellation. +> +> To learn more, see +> [Coroutine cancellation](https://kotlinlang.org/docs/cancellation-and-timeouts.html). +{style="note"} + +> For the full example, see [%example_name%](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/%example_name%). + +## Limitations + +This plugin is only fully supported on `CIO` and `Netty` engines. Engines based on servlets (or other unsupported engines) +cannot detect client disconnects reliably. Cancellation can only be detected when the server attempts to write a +response. \ No newline at end of file diff --git a/topics/server-oauth.md b/topics/server-oauth.md index 460d48cf4..d53190ed3 100644 --- a/topics/server-oauth.md +++ b/topics/server-oauth.md @@ -38,7 +38,7 @@ request the resource. ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="14,28-32,104,131-132"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="14,29-33,115,141-143"} ## OAuth authorization flow {id="flow"} @@ -77,7 +77,7 @@ For example, to install an `oauth` provider with the name "auth-oauth-google" it ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="9-10,28-29,34-37,57-58,104"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="9-10,29-30,35-38,65-66,115"} ## Configure OAuth {id="configure-oauth"} @@ -111,7 +111,7 @@ JSON serializer is required to deserialize received JSON data [after a request t ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="21-27"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="22-28"} The client instance is passed to the `main` [module function](server-modules.md) to have the capability to create a separate client instance in a server [test](server-testing.md). @@ -119,7 +119,7 @@ client instance in a server [test](server-testing.md). ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="29,104"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="30,115"} ### Step 2: Configure the OAuth provider {id="configure-oauth-provider"} @@ -128,13 +128,14 @@ The code snippet below shows how to create and configure the `oauth` provider wi ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="33-58"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="34-66"} * The `urlProvider` specifies a [redirect route](#redirect-route) that will be invoked when authorization is completed. > Make sure that this route is added to a list of [**Authorised redirect URIs**](#authorization-credentials). * `providerLookup` allows you to specify OAuth settings for a required provider. These settings are represented by the [OAuthServerSettings](https://api.ktor.io/ktor-server-auth/io.ktor.server.auth/-o-auth-server-settings/index.html) class and allow Ktor to make automatic requests to the OAuth server. +* The `fallback` property handles OAuth flow errors by responding with a redirect or custom response. * The `client` property specifies the [HttpClient](#create-http-client) used by Ktor to make requests to the OAuth server. @@ -149,7 +150,7 @@ in [providerLookup](#configure-oauth-provider). ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="59-63,79,103"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="67-71,87,114"} A user will see the authorization page with the level of permissions required for a Ktor application. These permissions depend on `defaultScopes` specified in [providerLookup](#configure-oauth-provider). @@ -167,7 +168,7 @@ returned by the OAuth server. ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="59-79,103"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="67-87,114"} In this example, the following actions are performed after receiving a token: @@ -186,14 +187,14 @@ Create a new function called `getPersonalGreeting` which will make the request a ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="106-113"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="117-124"} Then, you can call the function within a `get` route to retrieve a user's information: ```kotlin ``` -{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="96-102"} +{src="snippets/auth-oauth-google/src/main/kotlin/com/example/oauth/google/Application.kt" include-lines="104-110"} For the complete runnable example, see [auth-oauth-google](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/auth-oauth-google). diff --git a/topics/server-responses.md b/topics/server-responses.md index df4cb2ad9..ca33f0d16 100644 --- a/topics/server-responses.md +++ b/topics/server-responses.md @@ -4,18 +4,22 @@ Learn how to send different types of responses. -Ktor allows you to handle incoming [requests](server-requests.md) and send responses inside [route handlers](server-routing.md#define_route). You can send different types of responses: plain text, HTML documents and templates, serialized data objects, and so on. For each response, you can also configure various [response parameters](#parameters), such as a content type, headers, and cookies. +Ktor allows you to handle incoming [requests](server-requests.md) and send responses inside [route handlers](server-routing.md#define_route). You can send +different types of responses: plain text, HTML documents and templates, serialized data objects, and so on. You can also +configure various [response parameters](#parameters), such as content type, headers, cookies, and the status code. Inside a route handler, the following API is available for working with responses: -* A set of functions targeted for [sending a specific content type](#payload), for example, [call.respondText](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-text.html), [call.respondHtml](https://api.ktor.io/ktor-server-html-builder/io.ktor.server.html/respond-html.html), and so on. -* The [call.respond](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) function that allows you to [send any data](#payload) inside a response. For example, with the enabled [ContentNegotiation](server-serialization.md) plugin, you can send a data object serialized in a specific format. -* The [call.response](https://api.ktor.io/ktor-server-core/io.ktor.server.application/-application-call/response.html) property that returns the [ApplicationResponse](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/index.html) object that provides access to [response parameters](#parameters) and allows you to set the status code, add headers, and configure cookies. -* The [call.respondRedirect](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-redirect.html) provides the capability to add redirects. +* A set of functions for [sending specific content types](#payload), such as [`call.respondText()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-text.html) and [`call.respondHtml()`](https://api.ktor.io/ktor-server-html-builder/io.ktor.server.html/respond-html.html). +* The [`call.respond()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) function that allows you to [send any data type](#payload) inside a response. When the [ContentNegotiation](server-serialization.md) plugin is installed, you can send a data object serialized in a specific format. +* The [`call.response()`](https://api.ktor.io/ktor-server-core/io.ktor.server.application/-application-call/response.html) property that returns the [`ApplicationResponse`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/index.html) object, providing access to [response parameters](#parameters) for setting the status code, adding headers, and configuring cookies. +* The [`call.respondRedirect()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-redirect.html) function for sending redirect responses. ## Set response payload {id="payload"} + ### Plain text {id="plain-text"} -To send a plain text in a response, use the [call.respondText](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-text.html) function: + +To send plain text, use the [`call.respondText()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-text.html) function: ```kotlin get("/") { call.respondText("Hello, world!") @@ -23,21 +27,36 @@ get("/") { ``` ### HTML {id="html"} -Ktor provides two main ways to send HTML responses to a client: -* By building HTML using Kotlin HTML DSL. -* By using JVM template engines, such as FreeMarker, Velocity, and so on. -To send HTML build using Kotlin DSL, use the [call.respondHtml](https://api.ktor.io/ktor-server-html-builder/io.ktor.server.html/respond-html.html) function: +Ktor provides two main mechanisms for generating HTML responses: +* Building HTML using the Kotlin HTML DSL. +* Rendering templates using JVM template engines such as [FreeMarker](https://freemarker.apache.org/) or [Velocity](https://velocity.apache.org/engine/). + +#### Full HTML documents + +To send full HTML documents built with Kotlin DSL, use the [`call.respondHtml()`](https://api.ktor.io/ktor-server-html-builder/io.ktor.server.html/respond-html.html) function: + +```kotlin +``` +{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="13-27"} + +#### Partial HTML fragments + +If you need to return only a fragment of HTML, without wrapping it in ``, ``, or ``, you can use +`call.respondHtmlFragment()`: + ```kotlin ``` -{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="12-28"} +{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="28-35"} -To send a template in a response, call the [call.respond](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) function with a specific content ... +#### Templates + +To send a template in a response, use the [`call.respond()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) function with a specific content: ```kotlin ``` {src="snippets/freemarker/src/main/kotlin/com/example/Application.kt" include-lines="16-19"} -... or use an appropriate [call.respondTemplate](https://api.ktor.io/ktor-server-freemarker/io.ktor.server.freemarker/respond-template.html) function: +You can also use the [`call.respondTemplate()`](https://api.ktor.io/ktor-server-freemarker/io.ktor.server.freemarker/respond-template.html) function: ```kotlin get("/index") { val sampleUser = User(1, "John") @@ -48,64 +67,122 @@ You can learn more from the [](server-templating.md) help section. ### Object {id="object"} -To enable serialization of data objects in Ktor, you need to install the [ContentNegotiation](server-serialization.md) plugin and register a required converter (for example, JSON). Then, you can use the [call.respond](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) function to pass a data object in a response: + +To enable serialization of data objects in Ktor, you need to install the [ContentNegotiation](server-serialization.md) +plugin and register a required converter (for example, JSON). Then, you can use the [`call.respond()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond.html) +function to pass a data object in a response: ```kotlin ``` {src="snippets/json-kotlinx/src/main/kotlin/jsonkotlinx/Application.kt" include-lines="32-36"} -You can find the full example here: [json-kotlinx](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/json-kotlinx). +For the full example, see [json-kotlinx](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/json-kotlinx). [//]: # (TODO: Check link for LocalPathFile) ### File {id="file"} -To respond to a client with a content of a file, you have two options: +To respond to a client with the content of a file, you have two options: -- For `File` resources, use - the [call.respondFile](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-file.html) +- For a file represented as a `File` object, use + the [`call.respondFile()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-file.html) function. -- For `Path` resources, use the `call.respond()` function with - the [LocalPathContent](https://api.ktor.io/ktor-server-core/io.ktor.server.http.content/-local-path-content/index.html) +- For a file pointed by the given `Path` object, use the `call.respond()` function with + the [`LocalPathContent`](https://api.ktor.io/ktor-server-core/io.ktor.server.http.content/-local-path-content/index.html) class. -A code sample below shows how to send a specified file in a response and make this file downloadable by adding the `Content-Disposition` [header](#headers): +The example below shows how to send a file and make it downloadable by adding the `Content-Disposition` [header](#headers): ```kotlin ``` {src="snippets/download-file/src/main/kotlin/com/example/DownloadFile.kt" include-lines="3-35"} -Note that this sample has two plugins installed: -- [PartialContent](server-partial-content.md) enables the server to respond to requests with the `Range` header and send only a portion of content. -- [AutoHeadResponse](server-autoheadresponse.md) provides the ability to automatically respond to `HEAD` request for every route that has a `GET` defined. This allows the client application to determine the file size by reading the `Content-Length` header value. +Note that this sample uses two plugins: +- [`PartialContent`](server-partial-content.md) enables the server to respond to requests with the `Range` header and send only a portion of content. +- [`AutoHeadResponse`](server-autoheadresponse.md) provides the ability to automatically respond to a ` HEAD ` request for every route that has a `GET` defined. This allows the client application to determine the file size by reading the `Content-Length` header value. For the full code sample, see [download-file](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/download-file). +### Resource + +You can serve a single resource from the classpath with the [`call.respondResource()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-resource.html) method. +This method accepts the path to the resource and sends a response constructed in the following way: +it reads the response body from the resource stream, and derives the `Content-Type` header from the file extension. + +The following example shows the method call in a route handler: + +```kotlin +routing { + get("/resource") { + call.respondResource("public/index.html") + } +} +``` + +In the example above, since the resource extension is `.html`, the response will include the `Content-Type: text/html` header. +For convenience, you can pass components of the resource location, namely relative path and package, separately through the first and second parameters. +The following example resolves resources under the `assets` package based on the requested path: + +```kotlin +get("/assets/{rest-path...}") { + var path = call.parameters["rest-path"] + if (path.isNullOrEmpty()) { + path = "index.html" + } + + try { + call.respondResource(path, "assets") { + application.log.info(this.contentType.toString()) + } + } catch (_: IllegalArgumentException) { + call.respond(HttpStatusCode.NotFound) + } +} +``` + +If the requested path after the `/assets` prefix is empty or `/`, the handler uses the default `index.html` resource to +respond. If no resource is found at the given path, `IllegalArgumentException` is thrown. +The previous code snippet mimics a more general solution — serving resources from a package with the [`staticResources()`](server-static-content.md#resources) method. + ### Raw payload {id="raw"} -If you need to send the raw body payload, use the [call.respondBytes](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-bytes.html) function. + +To send the raw body payload, use the [`call.respondBytes()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-bytes.html) function. ## Set response parameters {id="parameters"} + ### Status code {id="status"} -To set a status code for a response, call [ApplicationResponse.status](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/status.html). You can pass a predefined status code value ... + +To set a status code for a response, call the [`ApplicationResponse.status()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/status.html) +function with a predefined status code value: + ```kotlin get("/") { call.response.status(HttpStatusCode.OK) } ``` -... or specify a custom status code: + +You can also specify custom status values: + ```kotlin get("/") { call.response.status(HttpStatusCode(418, "I'm a tea pot")) } ``` -Note that functions for sending a [payload](#payload) have overloads for specifying a status code. +> All payload-sending functions also provide overloads that accept a status code. +> +{style="note"} ### Content type {id="content-type"} -With the installed [ContentNegotiation](server-serialization.md) plugin, Ktor chooses a content type for a [response](#payload) automatically. If required, you can specify a content type manually by passing a corresponding parameter. For example, the `call.respondText` function in a code snippet below accepts `ContentType.Text.Plain` as a parameter: + +With the [ContentNegotiation](server-serialization.md) plugin installed, Ktor chooses a content type automatically. If required, you can +specify a content type manually by passing a corresponding parameter. + +In the example below, the `call.respondText()` function accepts `ContentType.Text.Plain` as a parameter: + ```kotlin get("/") { call.respondText("Hello, world!", ContentType.Text.Plain, HttpStatusCode.OK) @@ -113,51 +190,56 @@ get("/") { ``` ### Headers {id="headers"} -There are several ways to send specific headers in a response: -* Add a header to the [ApplicationResponse.headers](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/headers.html) collection: + +You can add headers to a response in several ways: +* Modify the [`ApplicationResponse.headers`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/headers.html) collection: ```kotlin get("/") { call.response.headers.append(HttpHeaders.ETag, "7c876b7e") } ``` -* Call the [ApplicationResponse.header](https://api.ktor.io/ktor-server-core/io.ktor.server.response/header.html) function: +* Use the [`ApplicationResponse.header()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/header.html) function: ```kotlin get("/") { call.response.header(HttpHeaders.ETag, "7c876b7e") } ``` -* Use functions dedicated to specifying concrete headers, for example, `ApplicationResponse.etag`, `ApplicationResponse.link`, and so on. +* Use convenience functions for specific headers, such as `ApplicationResponse.etag`, `ApplicationResponse.link`, and others. ```kotlin get("/") { call.response.etag("7c876b7e") } ``` -* To add a custom header, pass its name as a string value to any function mentioned above, for example: +* Add custom headers by passing raw string names: ```kotlin get("/") { call.response.header("Custom-Header", "Some value") } ``` -> To add the standard `Server` and `Date` headers into each response, install the [DefaultHeaders](server-default-headers.md) plugin. +> To include standard `Server` and `Date` headers automatically, install the [DefaultHeaders](server-default-headers.md) plugin. > {type="tip"} ### Cookies {id="cookies"} -To configure cookies sent in a response, use the [ApplicationResponse.cookies](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/cookies.html) property: + +To configure cookies sent in a response, use the [`ApplicationResponse.cookies`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/cookies.html) property: ```kotlin get("/") { call.response.cookies.append("yummy_cookie", "choco") } ``` -Ktor also provides the capability to handle sessions using cookies. You can learn more from the [Sessions](server-sessions.md) section. + +> Ktor also provides the ability to handle sessions using cookies. To learn more, see [](server-sessions.md). ## Redirects {id="redirect"} -To generate a redirection response, call the [respondRedirect](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-redirect.html) function: + +To generate a redirect response, use the [`call.respondRedirect()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-redirect.html) function: + ```kotlin get("/") { call.respondRedirect("/moved", permanent = true) diff --git a/topics/server-ssl.md b/topics/server-ssl.md index 4097f15c6..79553b21c 100644 --- a/topics/server-ssl.md +++ b/topics/server-ssl.md @@ -101,7 +101,7 @@ following [properties](server-configuration-file.topic#predefined-properties): ```shell ``` - {style="block" src="snippets/ssl-engine-main/src/main/resources/application.conf" include-lines="1-2,4-5,18"} + {style="block" src="snippets/ssl-engine-main/src/main/resources/application.conf" include-lines="1-2,4-5,21"} @@ -120,14 +120,14 @@ following [properties](server-configuration-file.topic#predefined-properties): ```shell ``` - {style="block" src="snippets/ssl-engine-main/src/main/resources/application.conf" include-lines="1,10-18"} + {style="block" src="snippets/ssl-engine-main/src/main/resources/application.conf" include-lines="1,10-21"} ```yaml ``` - {style="block" src="snippets/ssl-engine-main/src/main/resources/_application.yaml" include-lines="1,9-14"} + {style="block" src="snippets/ssl-engine-main/src/main/resources/_application.yaml" include-lines="1,9-17"} diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md new file mode 100644 index 000000000..da5e809f8 --- /dev/null +++ b/topics/whats-new-340.md @@ -0,0 +1,460 @@ +[//]: # (title: What's new in Ktor 3.4.0) + + + +[//]: # (TODO: Change date) + +_[Released: December XX, 2025](releases.md#release-details)_ + +## Ktor Server + +### OAuth fallback for error handling + +Ktor 3.4.0 introduces a new `fallback()` function for the [OAuth](server-oauth.md) authentication provider. +The fallback is invoked when the OAuth flow fails with `AuthenticationFailedCause.Error`, such as token exchange +failures, network issues, or response parsing errors. + +Previously, you might have used `authenticate(optional = true)` on OAuth-protected routes to bypass OAuth failures. +However, optional authentication only suppresses challenges when no credentials are provided and does not cover actual +OAuth errors. + +The new `fallback()` function provides a dedicated mechanism for handling these scenarios. If the fallback does not +handle the call, Ktor returns `401 Unauthorized`. + +To configure a fallback, define it inside the `oauth` block: + +```kotlin +install(Authentication) { + oauth("login") { + client = ... + urlProvider = ... + providerLookup = { ... } + fallback = { cause -> + if (cause is OAuth2RedirectError) { + respondRedirect("/login-after-fallback") + } else { + respond(HttpStatusCode.Forbidden, cause.message) + } + } + } +} +``` + +### Zstd compression support + +[Ztsd](https://github.com/facebook/zstd) compression is now supported by the [Compression](server-compression.md) +plugin. +`Zstd` is a fast compression algorithm that offers high compression ratios and low compression times, and has a +configurable compression level. To enable it, specify the `zstd {}` block inside the `install(Compression) {}` block +with the desired configuration: + +```kotlin +install(Compression) { + zstd { + compressionLevel = 3 + ... + } +} +``` + +### SSL trust store settings in a configuration file + +Ktor now allows you to configure additional [SSL settings](server-ssl.md#config-file) for the server using the +application configuration file. You can specify a trust store, its corresponding password, and the list of enabled TLS +protocols directly in your configuration. + +You define these settings under the `ktor.security.ssl` section: + +```kotlin +// application.conf +ktor { + security { + ssl { + // ... + trustStore = truststore.jks + trustStorePassword = foobar + enabledProtocols = ["TLSv1.2", "TLSv1.3"] + } + } +} +``` + +From the code above: +- `trustStore` – the path to the trust store file containing trusted certificates. +- `trustStorePassword` – password for the trust store. +- `enabledProtocols` – a list of allowed TLS protocols. + +### HTML fragments for partial responses + +Ktor now provides a new `.respondHtmlFragment()` function for sending partial HTML responses. +This is useful when generating markup that does not require a full `` document, such as dynamic UI updates with +tools like HTMX. + +The new API is part of the [HTML DSL](server-html-dsl.md) plugin and allows you to return HTML rooted in any element: + +```kotlin +get("/books.html") { + call.respondHtmlFragment { + div("books") { + for (book in library.books()) { + bookItem() + } + } + } +} +``` + +### HTTP request lifecycle + +The new [`HttpRequestLifecycle` plugin](server-http-request-lifecycle.md) allows you to cancel inflight HTTP requests when the client disconnects. +This is useful when you need to cancel an inflight HTTP request for a long-running or resource-intensive request +when the client disconnects. + +Enable this feature by installing the `HttpRequestLifecycle` plugin and setting `cancelCallOnClose = true`: + +```kotlin +install(HttpRequestLifecycle) { + cancelCallOnClose = true +} + +routing { + get("/long-process") { + try { + while (isActive) { + delay(10_000) + logger.info("Very important work.") + } + call.respond("Completed") + } catch (e: CancellationException) { + logger.info("Cleaning up resources.") + } + } +} +``` + +When the client disconnects, the coroutine handling the request is canceled, and structured concurrency handles cleaning +all resources. Any `launch` or `async` coroutines started by the request are also canceled. +This is currently only supported for the `Netty` and `CIO` engine. + +### New method to respond with a resource + +The new [`call.respondResource()`](server-responses.md#resource) method works in a similar way to [`call.respondFile()`](server-responses.md#file), +but accepts a resource instead of a file to respond with. + +To serve a single resource from the classpath, use `call.respondResource()` and specify the resource path: + +```kotlin +routing { + get("/resource") { + call.respondResource("public/index.html") + } +} +``` + +### API Key authentication + +The new [API Key authentication plugin](server-api-key-auth.md) allows you to secure server routes using a shared secret +passed with each request, typically in an HTTP header. + +The `apiKey` provider integrates with Ktor’s [Authentication plugin](server-auth.md) and lets you validate incoming API +keys using custom logic, customize the header name, and protect specific routes with standard `authenticate` blocks: + +```kotlin +install(Authentication) { + apiKey("my-api-key") { + validate { apiKey -> + if (apiKey == "secret-key") { + UserIdPrincipal(apiKey) + } else { + null + } + } + } +} + +routing { + authenticate { + get("/") { + val principal = call.principal()!! + call.respondText("Key: ${principal.key}") + } + } +} +``` +API Key authentication can be used for service-to-service communication and other scenarios where a lightweight +authentication mechanism is sufficient. + +For more details and configuration options, see [](server-api-key-auth.md). + +## Core + +### Multiple header parsing + +The new `Headers.getSplitValues()` function simplifies working with headers that contain multiple values +in a single line. + +The `getSplitValues()` function returns all values for the given header and splits them using the specified separator +(`,` by default): + +```kotlin +val headers = headers { + append("X-Multi-Header", "1, 2") + append("X-Multi-Header", "3") +} + +val splitValues = headers.getSplitValues("X-Multi-Header")!! +// ["1", "2", "3"] +``` +By default, separators inside double-quoted strings are ignored, but you can change this by setting +`splitInsideQuotes = true`: + +```kotlin +val headers = headers { + append("X-Multi-Header", """a,"b,c",d""") +} + +val forceSplit = headers.getSplitValues("X-Quoted", splitInsideQuotes = true) +// ["a", "\"b", "c\"", "d"] +``` + +## Ktor Client + +### Authentication token cache control + +Prior to Ktor 3.4.0, applications using [Basic](client-basic-auth.md) and [Bearer authentication](client-bearer-auth.md) +providers could continue sending outdated tokens or credentials after a user logged out or updated their authentication +data. This happened because each provider internally caches the result of the `loadTokens()` function through +an internal component responsible for storing loaded authentication tokens, and this cache remained active until +manually cleared. + +Ktor 3.4.0 introduces new functions and configuration options that give you explicit and convenient control over token +caching behavior. + +#### Accessing and clearing authentication tokens + +You can now access authentication providers directly from the client and clear their cached tokens when needed. + +To clear the token for a specific provider, use the `.clearToken()` function: + +```kotlin +val provider = client.authProvider() +provider?.clearToken() +``` + +Retrieve all authentication providers: + +```kotlin +val providers = client.authProviders +``` + +To clear cached tokens from all providers that support token clearing (currently Basic and Bearer), use +the `HttpClient.clearAuthTokens()` function: + +```kotlin + // Clears all cached auth tokens on logout +fun logout() { + client.clearAuthTokens() + storage.deleteTokens() +} + +// Clears cached auth tokens when credentials are updated +fun updateCredentials(new: Credentials) { + storage.save(new) + client.clearAuthTokens() // Forces reload +} +``` + +#### Configuring token cache behavior + +A new `cacheTokens` configuration option has been added to both Basic and Bearer authentication providers. This allows +you to control whether tokens or credentials should be cached between requests. + +For example, you can disable caching when credentials are dynamically provided: + +```kotlin +basic { + cacheTokens = false // Loads credentials on every request + credentials { + getCurrentUserCredentials() + } +} +``` + +Disabling caching is especially useful when authentication data changes frequently or must always reflect the most +recent state. + +### Duplex streaming for OkHttp + +The OkHttp client engine now supports duplex streaming, enabling clients to send request body data and receive response +data simultaneously. + +Unlike regular HTTP calls where the request body must be fully sent before the response begins, duplex mode +supports bidirectional streaming, allowing the client to send and receive data concurrently. + +Duplex streaming is available for HTTP/2 connections and can be enabled using the new `duplexStreamingEnabled` property +in `OkHttpConfig`: + +```kotlin +val client = HttpClient(OkHttp) { + engine { + duplexStreamingEnabled = true + config { + protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) + } + } +} +``` + +### Apache5 connection manager configuration + +The Apache5 engine now supports configuring the connection manager directly using the new `configureConnectionManager {}` +function. + +This approach is recommended over the previous method using `customizeClient { setConnectionManager(...) }`. Using +`customizeClient` would replace the Ktor-managed connection manager, potentially bypassing engine settings, timeouts, +and other internal configuration. + + + +```kotlin +val client = HttpClient(Apache5) { + engine { + customizeClient { + setConnectionManager( + PoolingAsyncClientConnectionManagerBuilder.create() + .setMaxConnTotal(10_000) + .setMaxConnPerRoute(1_000) + .build() + ) + } + } +} +``` + +```kotlin +val client = HttpClient(Apache5) { + engine { + configureConnectionManager { + setMaxConnTotal(10_000) + setMaxConnPerRoute(1_000) + } + } +} +``` + + + +The new `configureConnectionManager {}` function keeps Ktor in control while allowing you to adjust parameters such as +maximum connections per route (`maxConnPerRoute`) and total maximum connections (`maxConnTotal`). + +### Dispatcher configuration for native client engines + +Native HTTP client engines (`Curl`, `Darwin`, and `WinHttp`) now respect the configured engine dispatcher and use +`Dispatchers.IO` by default. + +The `dispatcher` property has always been available on client engine configurations, but native engines previously +ignored it and always used `Dispatchers.Unconfined`. With this change, native engines use the configured dispatcher and +default to `Dispatchers.IO` when none is specified, aligning their behavior with other Ktor client engines. + +You can explicitly configure the dispatcher as follows: + +```kotlin +val client = HttpClient(Curl) { + engine { + dispatcher = Dispatchers.IO + } +} +``` +### HttpStatement execution using the engine dispatcher + +The `HttpStatement.execute {}` and `HttpStatement.body {}` blocks now run on the HTTP engine’s dispatcher +instead of the caller’s coroutine context. This prevents accidental blocking when these blocks are invoked from the +main thread. + +Previously, users had to manually switch dispatchers using `withContext` to avoid freezing the UI during I/O operations, +such as writing a streaming response to a file. With this change, Ktor automatically dispatches these blocks to the +engine’s coroutine context: + + + +```kotlin +client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> + withContext(Dispatchers.IO) { + val channel: ByteReadChannel = httpResponse.body() + // Process and write data + } +} +``` + +```kotlin +client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> + val channel: ByteReadChannel = httpResponse.body() + // Process and write data +} +``` + + +### Plugin and default request configuration replacement + +Ktor client configuration now provides more control over replacing existing settings at runtime. + +#### Replace plugin configuration + +The new `installOrReplace()` function installs a client plugin or replaces its existing configuration if the plugin is +already installed. This is useful when you need to reconfigure a plugin without manually removing it first. + +```kotlin +val client = HttpClient { + installOrReplace(ContentNegotiation) { + json() + } +} +``` + +In the above example, if `ContentNegotiation` is already installed, its configuration is replaced with the new one +provided in the block. + +#### Replace default request configuration + +The `defaultRequest()` function now accepts an optional `replace` parameter (default is `false`). When set to `true`, +the new configuration replaces any previously defined default request settings instead of merging with them. + +```kotlin +val client = HttpClient { + defaultRequest(replace = true) { + // ... + } +} +``` + +This allows you to explicitly override earlier default request configuration when composing or reusing client setups. + +## I/O + +### Stream bytes from a `ByteReadChannel` to a `RawSink` + +You can now use the new `ByteReadChannel.readTo()` function to read bytes from a channel and write them directly to a +specified `RawSink`. This function simplifies handling large responses or file downloads without intermediate buffers or +manual copying. + +The following example downloads a file and writes it to a new local file: + +```kotlin +val client = HttpClient(CIO) +val file = File.createTempFile("files", "index") +val stream = file.outputStream().asSink() +val fileSize = 100 * 1024 * 1024 + +runBlocking { + client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> + val channel: ByteReadChannel = httpResponse.body() + channel.readTo(stream) + } +} + +println("A file saved to ${file.path}") + +``` + + + diff --git a/v.list b/v.list index e43ebcf9c..e1cc71730 100644 --- a/v.list +++ b/v.list @@ -4,7 +4,7 @@ - +