From ebce6007d10a260342febb181bdbdf672f5bf6b7 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Tue, 2 Dec 2025 15:10:49 +0100 Subject: [PATCH 01/27] KTOR-9028 Add docs for the OAuth fallback function (#722) --- codeSnippets/gradle.properties | 2 +- .../com/example/oauth/google/Application.kt | 10 +++++ ktor.tree | 1 + topics/server-oauth.md | 19 ++++----- topics/whats-new-340.md | 39 +++++++++++++++++++ 5 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 topics/whats-new-340.md diff --git a/codeSnippets/gradle.properties b/codeSnippets/gradle.properties index b1cd9ae71..95205047a 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.2 +ktor_version = 3.4.0-eap-1454 kotlinx_coroutines_version = 1.10.1 kotlinx_serialization_version = 1.8.0 kotlin_css_version = 1.0.0-pre.721 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/ktor.tree b/ktor.tree index 5d7f71849..91b422d68 100644 --- a/ktor.tree +++ b/ktor.tree @@ -392,6 +392,7 @@ + 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/whats-new-340.md b/topics/whats-new-340.md new file mode 100644 index 000000000..a15b0bfcf --- /dev/null +++ b/topics/whats-new-340.md @@ -0,0 +1,39 @@ +[//]: # (title: What's new in Ktor 3.4.0) + + + +_[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) + } + } + } +} +``` From 6ce1e9228317c2f17c85dd98adcc1eeb8fcb107b Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Fri, 5 Dec 2025 11:06:59 +0100 Subject: [PATCH 02/27] KTOR-8952 Documentation for multiple header parsing (#727) * add multiple header parsing to "what's new" * add multiple header parsing to client-responses.md --- topics/client-responses.md | 28 +++++++++++++++++++++++++--- topics/whats-new-340.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/topics/client-responses.md b/topics/client-responses.md index 3a4ecf129..db70ea6e2 100644 --- a/topics/client-responses.md +++ b/topics/client-responses.md @@ -39,16 +39,38 @@ 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 +Additionally, `HttpResponse` 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. +#### Splitting 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"} ### Raw body {id="raw"} diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index a15b0bfcf..1a12e9c70 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -37,3 +37,33 @@ install(Authentication) { } } ``` + +## Core + +### Multiple header parsing + +A new function, `Headers.getSplitValues()`, has been added to simplify working with headers that contain multiple values +in a single line. + +`getSplitValues()` 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 this can be changed 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"] +``` \ No newline at end of file From bdeb6f4a7e84eaf67488febe1386bdd948040ec1 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Mon, 8 Dec 2025 15:07:01 +0100 Subject: [PATCH 03/27] fix: KTOR-8880 --- topics/server-compression.md | 27 ++++++++++++++++++++++----- topics/whats-new-340.md | 16 ++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/topics/server-compression.md b/topics/server-compression.md index 519023f0e..06deb7ab5 100644 --- a/topics/server-compression.md +++ b/topics/server-compression.md @@ -17,7 +17,7 @@ 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. +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), or even compress data based on specific request parameters. > Note that the `%plugin_name%` plugin does not currently support `SSE` responses. > @@ -51,6 +51,7 @@ To enable only specific encoders, call the corresponding extension functions, fo install(Compression) { gzip() deflate() + ztsd() } ``` @@ -64,10 +65,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 +98,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 +136,19 @@ install(Compression) { } ``` +## Compression Level {id="compression_level"} + +`Ztsd` comes with configurable compression level. By default, it is set to `3`, and can be configured by assigning the +desired level to 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/whats-new-340.md b/topics/whats-new-340.md index 1a12e9c70..3b96afe84 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -38,6 +38,22 @@ install(Authentication) { } ``` +### 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 `compression` block with the desired +configuration: + +```kotlin +install(Compression) { + zstd { + compressionLevel = 3 + ... + } +} +``` + ## Core ### Multiple header parsing From e748739ee5d39a5328cbe844667b27cc6e724258 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Wed, 10 Dec 2025 13:56:12 +0100 Subject: [PATCH 04/27] KTOR-8949, KTOR-8950, KTOR-8948 Docs for client auth cache control (#732) * Add a what's new entry for client auth token cache control * document token cache control in client-auth and client-basic-auth * add a step for the cacheTokens option in client-bearer-auth.md --- topics/client-auth.md | 71 ++++++++++++++++++++++++++++++++++++ topics/client-basic-auth.md | 42 +++++++++++++++------ topics/client-bearer-auth.md | 18 ++++++++- topics/whats-new-340.md | 67 +++++++++++++++++++++++++++++++++- 4 files changed, 185 insertions(+), 13 deletions(-) diff --git a/topics/client-auth.md b/topics/client-auth.md index 763585840..9487586d3 100644 --- a/topics/client-auth.md +++ b/topics/client-auth.md @@ -109,3 +109,74 @@ 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 // Reload credentials for every request + credentials { + loadCurrentCredentials() + } +} +``` + +Disabling token caching is especially useful when authentication data changes frequently or must always 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..6d1b3acb4 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 // Reload credentials for every request + // ... + } + ``` + Disabling caching is useful when credentials may change during the client session or must always 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 the full example, 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..65eb0fd15 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 // Reload 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/whats-new-340.md b/topics/whats-new-340.md index 3b96afe84..5e07cf58a 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -82,4 +82,69 @@ val headers = headers { val forceSplit = headers.getSplitValues("X-Quoted", splitInsideQuotes = true) // ["a", "\"b", "c\"", "d"] -``` \ No newline at end of file +``` + +## 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 +`AuthTokenHolder`, 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 +`HttpClient.clearAuthTokens()`: + +```kotlin + // Clear all cached auth tokens on logout +fun logout() { + client.clearAuthTokens() + storage.deleteTokens() +} + +// Clear cached auth tokens when credentials are updated +fun updateCredentials(new: Credentials) { + storage.save(new) + client.clearAuthTokens() // Force 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 // Load credentials on every request + credentials { + getCurrentUserCredentials() + } +} +``` + +Disabling caching is especially useful when authentication data changes frequently or must always reflect the most +recent state. \ No newline at end of file From 9663db3e4dc70820f776393d3ed74d3fed65e6ed Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Wed, 10 Dec 2025 14:21:35 +0100 Subject: [PATCH 05/27] KTOR-9068 Documentation for OkHttp duplex streaming config property (#734) --- .../snippets/_misc_client/OkHttpConfig.kt | 1 + topics/whats-new-340.md | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/codeSnippets/snippets/_misc_client/OkHttpConfig.kt b/codeSnippets/snippets/_misc_client/OkHttpConfig.kt index 5aac11ba5..8cab3604f 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/topics/whats-new-340.md b/topics/whats-new-340.md index 5e07cf58a..e4d811278 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -147,4 +147,24 @@ basic { ``` Disabling caching is especially useful when authentication data changes frequently or must always reflect the most -recent state. \ No newline at end of file +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)) + } + } +} +``` \ No newline at end of file From 6f510315ac9c6006396a44d277f27aef697e0cb6 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Wed, 10 Dec 2025 19:14:27 +0100 Subject: [PATCH 06/27] KTOR-9149 Add documentation for SSL trust store settings in a config file (#735) --- .../src/main/resources/_application.yaml | 5 +++- .../src/main/resources/application.conf | 3 +++ topics/server-ssl.md | 6 ++--- topics/whats-new-340.md | 26 +++++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) 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/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 index e4d811278..e27579ac5 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -54,6 +54,32 @@ install(Compression) { } ``` +### 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"] + } + } +} +``` + +- `trustStore` – the path to the trust store file containing trusted certificates. +- `trustStorePassword` – password for the trust store. +- `enabledProtocols` – a list of allowed TLS protocols. + ## Core ### Multiple header parsing From 5ef7f8856c75c16225f32ce877dd25d4ed8e9cec Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Mon, 15 Dec 2025 11:21:58 +0100 Subject: [PATCH 07/27] KTOR-9150 Documentation for Partial HTML response (#736) * KTOR-9150 Add documentation for partial HTML response * fix formatting, grammar, and language in server-responses.md --- codeSnippets/gradle.properties | 2 +- .../main/kotlin/com/example/Application.kt | 7 ++ topics/server-html-dsl.md | 40 ++++++- topics/server-responses.md | 110 ++++++++++++------ topics/whats-new-340.md | 20 ++++ 5 files changed, 140 insertions(+), 39 deletions(-) diff --git a/codeSnippets/gradle.properties b/codeSnippets/gradle.properties index 95205047a..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.4.0-eap-1454 +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/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/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-responses.md b/topics/server-responses.md index df4cb2ad9..22e26b45f 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 or Velocity. + +#### Full HTML documents + +To send HTML built using 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="12-28"} +{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()`: -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 ... +```kotlin +``` +{src="snippets/html/src/main/kotlin/com/example/Application.kt" include-lines="28-35"} + +#### 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,80 @@ 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) + 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) + 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: +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 `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. +- [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). ### 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 [`ApplicationResponse.status()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/-application-response/status.html) 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 +148,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/whats-new-340.md b/topics/whats-new-340.md index e27579ac5..71b91b141 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -80,6 +80,26 @@ ktor { - `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() + } + } + } +} +``` + ## Core ### Multiple header parsing From a8533c019d8601e406d2e306c05d33d943654eb4 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Mon, 15 Dec 2025 15:35:14 +0100 Subject: [PATCH 08/27] KTOR-9185 Http Request Lifecycle (#733) * fix: add Whats New section * Apply suggestions from code review Co-authored-by: Vik Nikolova * KTOR-9185 Add a new topic and project sample for the HttpRequestLifecycle plugin --------- Co-authored-by: Vik Nikolova --- codeSnippets/settings.gradle.kts | 1 + .../server-http-request-lifecycle/README.md | 14 ++++ .../build.gradle.kts | 30 +++++++++ .../main/kotlin/com/example/Application.kt | 37 ++++++++++ .../src/main/resources/application.conf | 8 +++ .../src/main/resources/logback.xml | 12 ++++ .../src/test/kotlin/ApplicationTest.kt | 33 +++++++++ ktor.tree | 1 + topics/server-http-request-lifecycle.md | 67 +++++++++++++++++++ topics/whats-new-340.md | 34 +++++++++- 10 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/README.md create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/build.gradle.kts create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/application.conf create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/src/main/resources/logback.xml create mode 100644 codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt create mode 100644 topics/server-http-request-lifecycle.md 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/server-http-request-lifecycle/README.md b/codeSnippets/snippets/server-http-request-lifecycle/README.md new file mode 100644 index 000000000..f5d730daf --- /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..323402104 --- /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 number 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..0b48b2cab --- /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/ktor.tree b/ktor.tree index 91b422d68..1b64f9919 100644 --- a/ktor.tree +++ b/ktor.tree @@ -88,6 +88,7 @@ accepts-web-file-names="request-validation.html"/> + diff --git a/topics/server-http-request-lifecycle.md b/topics/server-http-request-lifecycle.md new file mode 100644 index 000000000..52a550187 --- /dev/null +++ b/topics/server-http-request-lifecycle.md @@ -0,0 +1,67 @@ +[//]: # (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 `cancelCallOnClose` 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/whats-new-340.md b/topics/whats-new-340.md index 71b91b141..73b698048 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -40,7 +40,8 @@ install(Authentication) { ### Zstd compression support -[Ztsd](https://github.com/facebook/zstd) compression is now supported by the [Compression](server-compression.md) plugin. +[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 `compression` block with the desired configuration: @@ -100,6 +101,37 @@ get("/books.html") { } ``` +### 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. This can be enabled 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. + ## Core ### Multiple header parsing From 2d8ea5808d350babd187a73cbd78138219d1f2b2 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Mon, 15 Dec 2025 16:00:45 +0100 Subject: [PATCH 09/27] fix typo --- topics/whats-new-340.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index 73b698048..b487e3fcf 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -130,7 +130,7 @@ routing { 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. +This is currently only supported for the `Netty` and `CIO` engine. ## Core From c58e767fd00bcf5f9c46a435f4f14c43c0b4c065 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Tue, 16 Dec 2025 14:05:55 +0100 Subject: [PATCH 10/27] KTOR-8900 Documentation for Apache5 connection manager config (#739) --- .../snippets/_misc_client/Apache5Create.kt | 10 +++++ topics/client-engines.md | 7 ++- topics/whats-new-340.md | 44 ++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) 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/topics/client-engines.md b/topics/client-engines.md index e6f3dfcdb..1cc556dfb 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"} diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index b487e3fcf..4658cf82b 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -245,4 +245,46 @@ val client = HttpClient(OkHttp) { } } } -``` \ No newline at end of file +``` + +### 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`). From 3ae05c1c0e30b4672d7b5762197fd5465cce896e Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Tue, 16 Dec 2025 14:10:33 +0100 Subject: [PATCH 11/27] KTOR-8902 Add documentation for replacing plugin configuration (#740) --- .../_misc_client/InstallOrReplacePlugin.kt | 8 ++++ topics/client-default-request.md | 26 +++++++++- topics/client-plugins.md | 47 ++++++++++++++----- topics/whats-new-340.md | 36 ++++++++++++++ 4 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 codeSnippets/snippets/_misc_client/InstallOrReplacePlugin.kt 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/topics/client-default-request.md b/topics/client-default-request.md index 56a067a85..fb6e7f2e7 100644 --- a/topics/client-default-request.md +++ b/topics/client-default-request.md @@ -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 `DefaultRequest` has already been installed, you can replace its existing configuration in one of the following ways: + +- Use the `replace` parameter of `defaultRequest()`: + +```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-plugins.md b/topics/client-plugins.md index 26b0a5772..ae467765a 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 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 `installOrReplace()`: + +```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/whats-new-340.md b/topics/whats-new-340.md index 4658cf82b..be5db29b4 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -288,3 +288,39 @@ val client = HttpClient(Apache5) { 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`). + + +### 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 being merged with them. + +```kotlin +val client = HttpClient { + defaultRequest(replace = true) { + // Configure default request parameters + } +} +``` + +This allows you to explicitly override earlier default request configuration when composing or reusing client setups. \ No newline at end of file From 4c69ee165313b367269140349c6c053ab3c58a0d Mon Sep 17 00:00:00 2001 From: Nathan Fallet Date: Tue, 16 Dec 2025 18:37:45 +0100 Subject: [PATCH 12/27] KTOR-9175 Add API Key auth documentation (#737) * KTOR-9175 Add api key auth documentation --------- Co-authored-by: Vik Nikolova --- ktor.tree | 3 + topics/server-api-key-auth.md | 250 ++++++++++++++++++++++++++++++++++ topics/server-auth.md | 3 +- 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 topics/server-api-key-auth.md diff --git a/ktor.tree b/ktor.tree index 1b64f9919..4c0c31140 100644 --- a/ktor.tree +++ b/ktor.tree @@ -146,6 +146,9 @@ + diff --git a/topics/server-api-key-auth.md b/topics/server-api-key-auth.md new file mode 100644 index 000000000..905155f13 --- /dev/null +++ b/topics/server-api-key-auth.md @@ -0,0 +1,250 @@ +[//]: # (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: + + + +## 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"} + +To get a general idea of how to configure different authentication providers in Ktor, see [](server-auth.md#configure). +In this section, we'll see the configuration specifics of the `apiKey` authentication provider. + +### 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 -> + // Validate and return principal + } +} +``` + +### 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 -> + // Look up the key in 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) + + // Check if 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}") + } + } +} +``` + +## Complete 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 (e.g., UUID, 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"} From 61bd4fa596cdb3ebd632448971d1cbb5b44c7b8b Mon Sep 17 00:00:00 2001 From: Aleksei Tirman Date: Thu, 18 Dec 2025 14:00:47 +0200 Subject: [PATCH 13/27] KTOR-8959 Add docs for the `respondResource` method (#738) --- cfg/glossary.xml | 3 +++ topics/server-responses.md | 48 ++++++++++++++++++++++++++++++++++---- topics/whats-new-340.md | 16 +++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) 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/topics/server-responses.md b/topics/server-responses.md index 22e26b45f..9c4d8d37e 100644 --- a/topics/server-responses.md +++ b/topics/server-responses.md @@ -84,11 +84,11 @@ For the full example, see [json-kotlinx](https://github.com/ktorio/ktor-document 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. The example below shows how to send a file and make it downloadable by adding the `Content-Disposition` [header](#headers): @@ -104,6 +104,46 @@ Note that this sample uses two plugins: 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 will use the default `index.html` resource to respond. If no resource is found at the given path, `IllegalArgumentException` will be 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"} diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index be5db29b4..2d8d62145 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -132,6 +132,22 @@ When the client disconnects, the coroutine handling the request is canceled, and 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. +### A 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") + } +} +``` + + ## Core ### Multiple header parsing From e69cc58f622680d869e44f6e29b6657cf4a53b38 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Thu, 18 Dec 2025 15:57:56 +0100 Subject: [PATCH 14/27] add a what's new entry on API key auth and fix dependencies --- topics/server-api-key-auth.md | 35 ++++++++++++++++++++++++++++++++++- topics/whats-new-340.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/topics/server-api-key-auth.md b/topics/server-api-key-auth.md index 905155f13..a348114f1 100644 --- a/topics/server-api-key-auth.md +++ b/topics/server-api-key-auth.md @@ -27,7 +27,40 @@ Ktor allows you to use API Key authentication for securing [routes](server-routi 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"} diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index 2d8d62145..1a1336901 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -147,6 +147,40 @@ routing { } ``` +### 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 From fb73e9f0b4f1fc610f2a26e3f3f4f295901d1143 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Fri, 19 Dec 2025 15:15:07 +0100 Subject: [PATCH 15/27] KTOR-9160 Add a what's new entry for native engines dispatcher config (#742) * add a what's new entry for dispatcher config for native client engines * fix broken API link and add a new one for engine configs --- topics/client-engines.md | 4 ++-- topics/whats-new-340.md | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/topics/client-engines.md b/topics/client-engines.md index 1cc556dfb..ecadff8c5 100644 --- a/topics/client-engines.md +++ b/topics/client-engines.md @@ -289,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 ``` @@ -316,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/whats-new-340.md b/topics/whats-new-340.md index 1a1336901..dacf9c76e 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -339,6 +339,24 @@ val client = HttpClient(Apache5) { 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 + } +} +``` ### Plugin and default request configuration replacement From 1a80da6642e001cbcaeac38b8fe11c34260e0f5b Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Fri, 19 Dec 2025 15:23:26 +0100 Subject: [PATCH 16/27] KTOR-9161 Add a what's new entry for running .execute on the engine dispatcher (#743) --- topics/whats-new-340.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index dacf9c76e..69c020e7f 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -357,6 +357,34 @@ val client = HttpClient(Curl) { } } ``` +### 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 From 87bcf758bc05cb8960a631a1766adef10805bfb0 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Fri, 19 Dec 2025 16:52:37 +0100 Subject: [PATCH 17/27] resolve comments from tw review --- .../snippets/_misc_client/OkHttpConfig.kt | 2 +- .../server-http-request-lifecycle/README.md | 2 +- .../main/kotlin/com/example/Application.kt | 4 +- .../src/test/kotlin/ApplicationTest.kt | 2 +- topics/client-auth.md | 16 ++++--- topics/client-basic-auth.md | 6 +-- topics/client-bearer-auth.md | 2 +- topics/client-default-request.md | 6 +-- topics/client-plugins.md | 8 ++-- topics/client-responses.md | 18 ++++---- topics/server-api-key-auth.md | 16 ++++--- topics/server-compression.md | 13 ++++-- topics/server-http-request-lifecycle.md | 6 +-- topics/server-responses.md | 18 ++++---- topics/whats-new-340.md | 44 +++++++++++-------- 15 files changed, 91 insertions(+), 72 deletions(-) diff --git a/codeSnippets/snippets/_misc_client/OkHttpConfig.kt b/codeSnippets/snippets/_misc_client/OkHttpConfig.kt index 8cab3604f..d7f2182b6 100644 --- a/codeSnippets/snippets/_misc_client/OkHttpConfig.kt +++ b/codeSnippets/snippets/_misc_client/OkHttpConfig.kt @@ -13,6 +13,6 @@ val client = HttpClient(OkHttp) { addNetworkInterceptor(interceptor) preconfigured = okHttpClientInstance - duplexStreamingEnabled = true // only available for HTTP/2 connections + duplexStreamingEnabled = true // Only available for HTTP/2 connections } } \ No newline at end of file diff --git a/codeSnippets/snippets/server-http-request-lifecycle/README.md b/codeSnippets/snippets/server-http-request-lifecycle/README.md index f5d730daf..7e5a5a2c2 100644 --- a/codeSnippets/snippets/server-http-request-lifecycle/README.md +++ b/codeSnippets/snippets/server-http-request-lifecycle/README.md @@ -3,7 +3,7 @@ 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. +> This sample is a part of the [`codeSnippets`](../../README.md) Gradle project. ## Running 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 index 323402104..aa026eb57 100644 --- 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 @@ -1,9 +1,9 @@ package com.example /* - Important: The contents of this file are referenced by line number in `server-http-request-lifecycle.md`. + 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. + line numbers in the code-block element of the referenced file. */ import io.ktor.server.application.* 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 index 0b48b2cab..b135344ac 100644 --- a/codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt +++ b/codeSnippets/snippets/server-http-request-lifecycle/src/test/kotlin/ApplicationTest.kt @@ -26,7 +26,7 @@ class ApplicationTest { } delay(300) - job.cancelAndJoin() // simulate client disconnect + job.cancelAndJoin() // Simulate client disconnect server.stop() } diff --git a/topics/client-auth.md b/topics/client-auth.md index 9487586d3..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) { @@ -130,6 +132,7 @@ 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 @@ -155,6 +158,7 @@ Clearing cached tokens is typically used in the following scenarios: - 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() @@ -171,12 +175,12 @@ For example, you can disable caching when credentials are dynamically provided: ```kotlin basic { - cacheTokens = false // Reload credentials for every request + cacheTokens = false // Reloads credentials for every request credentials { loadCurrentCredentials() } } ``` -Disabling token caching is especially useful when authentication data changes frequently or must always reflect the most +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 6d1b3acb4..1902c677e 100644 --- a/topics/client-basic-auth.md +++ b/topics/client-basic-auth.md @@ -71,16 +71,16 @@ authentication provider: ```kotlin basic { - cacheTokens = false // Reload credentials for every request + cacheTokens = false // Reloads credentials for every request // ... } ``` - Disabling caching is useful when credentials may change during the client session or must always reflect the latest + 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 the full example, see [client-auth-basic](https://github.com/ktorio/ktor-documentation/tree/%ktor_version%/codeSnippets/snippets/client-auth-basic). +> 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 65eb0fd15..68d2dfbdb 100644 --- a/topics/client-bearer-auth.md +++ b/topics/client-bearer-auth.md @@ -98,7 +98,7 @@ A Ktor client allows you to configure a token to be sent in the `Authorization` ```kotlin install(Auth) { bearer { - cacheTokens = false // Reload tokens for every request + cacheTokens = false // Reloads tokens for every request loadTokens { loadDynamicTokens() } diff --git a/topics/client-default-request.md b/topics/client-default-request.md index fb6e7f2e7..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"} @@ -50,9 +50,9 @@ val client = HttpClient(CIO) { ### Replace existing configuration {id="default_request_replace"} -If `DefaultRequest` has already been installed, you can replace its existing configuration in one of the following ways: +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 `defaultRequest()`: +- Use the `replace` parameter of the `defaultRequest()` function: ```kotlin val client = HttpClient(CIO) { diff --git a/topics/client-plugins.md b/topics/client-plugins.md index ae467765a..c58756a6b 100644 --- a/topics/client-plugins.md +++ b/topics/client-plugins.md @@ -8,7 +8,7 @@ Many applications require common functionality that is not part of the core appl [logging](client-logging.md), [serialization](client-serialization.md), or [authorization](client-auth.md). In Ktor, this functionality is provided by client _plugins_. -## Add plugin dependency {id="plugin-dependency"} +## Add plugin dependencies {id="plugin-dependency"} 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: @@ -20,7 +20,7 @@ Each plugin’s documentation specifies any required dependencies. ## Install a plugin {id="install"} -To install a plugin, pass it to the `install` function inside a [client configuration block](client-create-and-configure.md#configure-client). +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: @@ -31,7 +31,7 @@ For example, installing the `Logging` plugin looks as follows: ### 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 `installOrReplace()`: +can replace its configuration using the `installOrReplace()` function: ```kotlin ``` @@ -44,7 +44,7 @@ installed. 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 +For example, the [`Logging`](client-logging.md) plugin allows you to specify the logger, logging level, and condition for filtering log messages: ```kotlin diff --git a/topics/client-responses.md b/topics/client-responses.md index db70ea6e2..dea7d8409 100644 --- a/topics/client-responses.md +++ b/topics/client-responses.md @@ -42,19 +42,19 @@ The [ 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: +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. +* `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. -#### Splitting header values +#### 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: +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/") diff --git a/topics/server-api-key-auth.md b/topics/server-api-key-auth.md index a348114f1..edaed06c2 100644 --- a/topics/server-api-key-auth.md +++ b/topics/server-api-key-auth.md @@ -94,13 +94,14 @@ to [authenticate a specified route](#authenticate-route). ## Configure API Key authentication {id="configure"} -To get a general idea of how to configure different authentication providers in Ktor, see [](server-auth.md#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) +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 @@ -133,7 +134,7 @@ You can use `headerName` to specify a custom header: apiKey("api-key-header") { headerName = "X-Secret-Key" validate { key -> - // Validate and return principal + // ... } } ``` @@ -168,7 +169,7 @@ For multiple API keys, validate against a database: ```kotlin apiKey { validate { keyFromHeader -> - // Look up the key in database + // Looks up the key in the database val user = database.findUserByApiKey(keyFromHeader) user?.let { UserIdPrincipal(it.username) } } @@ -184,7 +185,7 @@ apiKey { validate { keyFromHeader -> val apiKey = database.findApiKey(keyFromHeader) - // Check if key exists, is active, and not expired + // Checks if the key exists, is active, and not expired if (apiKey != null && apiKey.isActive && apiKey.expiresAt > Clock.System.now() @@ -232,7 +233,7 @@ routing { } ``` -## Complete example {id="complete-example"} +## API Key authentication example {id="complete-example"} Here's a complete minimal example of API Key authentication: @@ -278,6 +279,7 @@ When implementing API Key authentication, consider the following best practices: 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 (e.g., UUID, base64-encoded random bytes). +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-compression.md b/topics/server-compression.md index 06deb7ab5..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`, `ztsd` 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. > @@ -138,8 +143,8 @@ install(Compression) { ## Compression Level {id="compression_level"} -`Ztsd` comes with configurable compression level. By default, it is set to `3`, and can be configured by assigning the -desired level to the `compressionLevel` property. +`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) { diff --git a/topics/server-http-request-lifecycle.md b/topics/server-http-request-lifecycle.md index 52a550187..ee6b3b720 100644 --- a/topics/server-http-request-lifecycle.md +++ b/topics/server-http-request-lifecycle.md @@ -23,7 +23,6 @@ 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 @@ -33,9 +32,8 @@ set the `cancelCallOnClose` property: ``` {src="snippets/server-http-request-lifecycle/src/main/kotlin/com/example/Application.kt" include-lines="9-10,18-22,37"} - -When `cancelCallOnClose` 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. +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 diff --git a/topics/server-responses.md b/topics/server-responses.md index 9c4d8d37e..ca33f0d16 100644 --- a/topics/server-responses.md +++ b/topics/server-responses.md @@ -30,11 +30,11 @@ get("/") { 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 or Velocity. +* 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 HTML built using Kotlin DSL, use the [`call.respondHtml()`](https://api.ktor.io/ktor-server-html-builder/io.ktor.server.html/respond-html.html) function: +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 ``` @@ -85,10 +85,10 @@ For the full example, see [json-kotlinx](https://github.com/ktorio/ktor-document To respond to a client with the content of a file, you have two options: - 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) + the [`call.respondFile()`](https://api.ktor.io/ktor-server-core/io.ktor.server.response/respond-file.html) function. - 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) + the [`LocalPathContent`](https://api.ktor.io/ktor-server-core/io.ktor.server.http.content/-local-path-content/index.html) class. The example below shows how to send a file and make it downloadable by adding the `Content-Disposition` [header](#headers): @@ -98,8 +98,8 @@ The example below shows how to send a file and make it downloadable by adding th {src="snippets/download-file/src/main/kotlin/com/example/DownloadFile.kt" include-lines="3-35"} 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. +- [`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). @@ -141,7 +141,8 @@ get("/assets/{rest-path...}") { } ``` -If the requested path after the `/assets` prefix is empty or `/`, the handler will use the default `index.html` resource to respond. If no resource is found at the given path, `IllegalArgumentException` will be thrown. +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. @@ -154,7 +155,8 @@ To send the raw body payload, use the [`call.respondBytes()`](https://api.ktor.i ### 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) with 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("/") { diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index 69c020e7f..0e40cd9c3 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -2,6 +2,8 @@ +[//]: # (TODO: Change date) + _[Released: December XX, 2025](releases.md#release-details)_ ## Ktor Server @@ -43,8 +45,8 @@ install(Authentication) { [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 `compression` block with the desired -configuration: +configurable compression level. To enable it, specify the `zstd {}` block inside the `install(Compression) {}` block +with the desired configuration: ```kotlin install(Compression) { @@ -77,6 +79,7 @@ ktor { } ``` +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. @@ -105,8 +108,9 @@ get("/books.html") { 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. This can be enabled by installing the `HttpRequestLifecycle` plugin and setting -`cancelCallOnClose = true`: +when the client disconnects. + +Enable this feature by installing the `HttpRequestLifecycle` plugin and setting `cancelCallOnClose = true`: ```kotlin install(HttpRequestLifecycle) { @@ -132,7 +136,7 @@ When the client disconnects, the coroutine handling the request is canceled, and 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. -### A method to respond with a resource +### 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. @@ -186,10 +190,11 @@ For more details and configuration options, see [](server-api-key-auth.md). ### Multiple header parsing -A new function, `Headers.getSplitValues()`, has been added to simplify working with headers that contain multiple values +The new `Headers.getSplitValues()` function simplifies working with headers that contain multiple values in a single line. -`getSplitValues()` returns all values for the given header and splits them using the specified separator (`,` by default): +The `getSplitValues()` function returns all values for the given header and splits them using the specified separator +(`,` by default): ```kotlin val headers = headers { @@ -200,7 +205,7 @@ val headers = headers { val splitValues = headers.getSplitValues("X-Multi-Header")!! // ["1", "2", "3"] ``` -By default, separators inside double-quoted strings are ignored, but this can be changed by setting +By default, separators inside double-quoted strings are ignored, but you can change this by setting `splitInsideQuotes = true`: ```kotlin @@ -219,7 +224,8 @@ val forceSplit = headers.getSplitValues("X-Quoted", splitInsideQuotes = true) 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 -`AuthTokenHolder`, and this cache remained active until manually cleared. +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. @@ -242,19 +248,19 @@ val providers = client.authProviders ``` To clear cached tokens from all providers that support token clearing (currently Basic and Bearer), use -`HttpClient.clearAuthTokens()`: +the `HttpClient.clearAuthTokens()` function: ```kotlin - // Clear all cached auth tokens on logout + // Clears all cached auth tokens on logout fun logout() { client.clearAuthTokens() storage.deleteTokens() } -// Clear cached auth tokens when credentials are updated +// Clears cached auth tokens when credentials are updated fun updateCredentials(new: Credentials) { storage.save(new) - client.clearAuthTokens() // Force reload + client.clearAuthTokens() // Forces reload } ``` @@ -267,7 +273,7 @@ For example, you can disable caching when credentials are dynamically provided: ```kotlin basic { - cacheTokens = false // Load credentials on every request + cacheTokens = false // Loads credentials on every request credentials { getCurrentUserCredentials() } @@ -280,8 +286,10 @@ 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. +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`: @@ -408,8 +416,8 @@ 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 being merged with them. +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 { From 1bea16b3ee3d6b3b5cf64651c613a5f815f0bdc6 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Fri, 19 Dec 2025 16:54:04 +0100 Subject: [PATCH 18/27] update ktor version in docs only --- help-versions.json | 2 +- project.ihp | 2 +- v.list | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/project.ihp b/project.ihp index 7831b1c3a..5395d644a 100644 --- a/project.ihp +++ b/project.ihp @@ -14,7 +14,7 @@ diff --git a/v.list b/v.list index e43ebcf9c..e1cc71730 100644 --- a/v.list +++ b/v.list @@ -4,7 +4,7 @@ - + From b598d6fa1d5f963fb6f7b09f0e81241b4e1601ea Mon Sep 17 00:00:00 2001 From: vnikolova Date: Fri, 2 Jan 2026 13:07:18 +0100 Subject: [PATCH 19/27] remove code comment --- topics/whats-new-340.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index 0e40cd9c3..3a7bd83c5 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -422,7 +422,7 @@ the new configuration replaces any previously defined default request settings i ```kotlin val client = HttpClient { defaultRequest(replace = true) { - // Configure default request parameters + // ... } } ``` From 7fcd193bfbdf2cef0609186db1f66caf59a8436e Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Mon, 5 Jan 2026 12:48:52 +0100 Subject: [PATCH 20/27] KTOR-9216 Documentation for ByteReadChannel.readTo util (#748) --- topics/client-responses.md | 74 +++++++++++++++++++++++++++++--------- topics/whats-new-340.md | 32 ++++++++++++++++- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/topics/client-responses.md b/topics/client-responses.md index dea7d8409..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. @@ -161,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 -> @@ -192,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/whats-new-340.md b/topics/whats-new-340.md index 3a7bd83c5..da5e809f8 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -427,4 +427,34 @@ val client = HttpClient { } ``` -This allows you to explicitly override earlier default request configuration when composing or reusing client setups. \ No newline at end of file +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}") + +``` + + + From 982c2ed135ce5358476a6bf51f47d8537f4fc057 Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Tue, 20 Jan 2026 13:03:07 +0100 Subject: [PATCH 21/27] KTOR-9159 Documentation for shared web source set support (#747) * add a what's new entry for shared source set support for js and wasmJS --- topics/whats-new-340.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index da5e809f8..3e99b1533 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -429,6 +429,34 @@ val client = HttpClient { This allows you to explicitly override earlier default request configuration when composing or reusing client setups. +### Shared source set support for `js` and `wasmJs` targets + +Ktor now supports [Kotlin’s shared `web` source set](https://kotlinlang.org/docs/whatsnew2220.html#shared-source-set-for-js-and-wasmjs-targets) +in multiplatform projects, allowing you to share Ktor dependencies between `js` and `wasmJs` targets. This makes it +easier to share web-specific client code, such as HTTP clients and engines, across JavaScript and Wasm/JS. + +In your +build.gradle.kts +file, you can declare Ktor dependencies in the `webMain` source set: + +```kotlin +kotlin { + sourceSets { + webMain.dependencies { + implementation("io.ktor:ktor-client-js:%ktor_version%") + } + } +} +``` + +You can then use APIs available to both `js` and `wasmJs` targets: + +```kotlin +// src/webMain/kotlin/Main.kt + +actual fun createClient(): HttpClient = HttpClient(Js) +``` + ## I/O ### Stream bytes from a `ByteReadChannel` to a `RawSink` From 4d59084c1ba73fee08e5b2c41ed1b3edcc4261ee Mon Sep 17 00:00:00 2001 From: Vik Nikolova Date: Tue, 20 Jan 2026 15:19:40 +0100 Subject: [PATCH 22/27] KTOR-9165 and KTOR-9193 Documentation for OpenAPI specification generation (#744) * KTOR-9165 and KTOR-9193 docs for runtime route annotations and openapi compiler extension * add a code sample project, update docs and add todos * update openapi compiler plugin configuration * update for technical accuracy and add links * update comment formatting, add metadata precedence and code inference rules * update sample project, compiler config options descriptions and invalid deps * update the swagger and openapi plugin topics * resolve comments and add a new section for Route.hide() --- codeSnippets/build.gradle | 8 +- codeSnippets/gradle.properties | 4 +- codeSnippets/settings.gradle.kts | 1 + .../snippets/openapi-spec-gen/README.md | 20 ++ .../openapi-spec-gen/build.gradle.kts | 40 +++ .../main/kotlin/com/example/Application.kt | 163 +++++++++ .../src/main/resources/application.conf | 8 + .../src/main/resources/logback.xml | 12 + .../src/test/kotlin/ApplicationTest.kt | 14 + .../gradle/libs.versions.toml | 2 +- .../build.gradle.kts | 2 +- .../build.gradle.kts | 2 +- labels.list | 4 +- topics/openapi-spec-generation.md | 339 ++++++++++++++---- topics/server-openapi.md | 55 ++- topics/server-swagger-ui.md | 58 ++- topics/whats-new-330.md | 5 +- topics/whats-new-340.md | 99 +++++ 18 files changed, 719 insertions(+), 117 deletions(-) create mode 100644 codeSnippets/snippets/openapi-spec-gen/README.md create mode 100644 codeSnippets/snippets/openapi-spec-gen/build.gradle.kts create mode 100644 codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt create mode 100644 codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf create mode 100644 codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml create mode 100644 codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt 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 dc49476a6..f5c1b9fd1 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.4.0-eap-1477 +ktor_version = 3.3.3 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 fc1e6f2e7..742f8d072 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -162,6 +162,7 @@ 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/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..48ad8f21a --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts @@ -0,0 +1,40 @@ +val ktor_version = "3.4.0-eap-1518" +val kotlin_version: String by project +val logback_version: String by project + +plugins { + application + kotlin("jvm") + id("io.ktor.plugin") version "3.3.3" +} + +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..5a491f8f8 --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,163 @@ +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.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/tutorial-server-db-integration/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml index 2e7fe41aa..da4fe9258 100644 --- a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml @@ -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..5134f82f5 100644 --- a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts @@ -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-websockets/build.gradle.kts b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts index b06e12807..8ddaf9971 100644 --- a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts @@ -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/labels.list b/labels.list index e4138ff33..7e15ed171 100644 --- a/labels.list +++ b/labels.list @@ -9,14 +9,12 @@ 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/topics/openapi-spec-generation.md b/topics/openapi-spec-generation.md index 960f83f54..58766b015 100644 --- a/topics/openapi-spec-generation.md +++ b/topics/openapi-spec-generation.md @@ -1,18 +1,29 @@ [//]: # (title: OpenAPI specification generation) - + + +

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

+

Code example: openapi

-Ktor provides experimental support for generating OpenAPI specifications directly from your Kotlin code. -This functionality is available via the Ktor Gradle plugin and can be combined with the [OpenAPI](server-openapi.md) +Ktor provides support for building OpenAPI specifications at runtime from one or more documentation sources. + +This functionality is available through: +* The OpenAPI compiler extension (included in the Ktor Gradle plugin), which analyzes routing code at compile time and +generates Kotlin code that registers OpenAPI metadata at runtime. +* The routing annotation runtime API, which attaches OpenAPI metadata directly to routes in the running application. + +You can use one or both and combine them with the [OpenAPI](server-openapi.md) and [SwaggerUI](server-swagger-ui.md) plugins to serve interactive API documentation. > The OpenAPI Gradle extension requires Kotlin 2.2.20. Using other versions may result in compilation @@ -20,9 +31,9 @@ and [SwaggerUI](server-swagger-ui.md) plugins to serve interactive API documenta > {style="note"} -## Add the Gradle plugin +## Add dependencies -To enable specification generation, apply the Ktor Gradle plugin to your project: +* To enable OpenAPI metadata generation, apply the Ktor Gradle plugin to your project: ```kotlin plugins { @@ -30,37 +41,59 @@ plugins { } ``` -## Configure the extension +* To use runtime route annotations, add the `%artifact_name%` artifact to your build script: -To configure the extension, use the `openApi` block inside the `ktor` extension in your -build.gradle.kts -file. You can provide metadata such as title, description, license, and contact information: + -```kotlin -ktor { - @OptIn(OpenApiPreview::class) - openApi { - title = "OpenAPI example" - version = "2.1" - summary = "This is a sample API" - description = "This is a longer description" - termsOfService = "https://example.com/terms/" - contact = "contact@example.com" - license = "Apache/1.0" - - // Location of the generated specification (defaults to openapi/generated.json) - target = project.layout.buildDirectory.file("open-api.json") - } -} -``` +## Configure the OpenAPI compiler extension {id="configure-the-extension"} -## Routing API introspection +The OpenAPI compiler extension controls how routing metadata is collected at compile time. +It does not define the final OpenAPI document itself. -The plugin can analyze your server routing DSL to infer basic path information, such as: +During compilation, the plugin generates Kotlin code that uses the OpenAPI runtime API to register metadata derived +from routing declarations, code patterns, and comments. -- The merged path (`/api/v1/users/{id}`). -- Path parameters. +General OpenAPI information — such as the API title, version, servers, security schemes, and detailed schemas — is supplied +at runtime [when the specification is generated](#generate-and-serve-the-specification). + +To configure the compiler plugin extension, use the `openApi {}` block inside the `ktor` extension in your +build.gradle.kts +file: + +```kotlin +``` +{src="snippets/openapi-spec-gen/build.gradle.kts" include-lines="20-26"} + +### Configuration options + + + +<code>enabled</code> +Enables or disables OpenAPI route annotation code generation. Defaults to false. + + +<code>codeInferenceEnabled</code> +Controls whether the compiler attempts to infer OpenAPI metadata from routing code. Defaults to true. +Disable this option if inference produces incorrect results, or you prefer to define metadata explicitly using +annotations. +For more details, see code inference rules. + + +<code>onlyCommented</code> +Limits metadata generation to routes that contain comment annotations. Defaults to false, meaning all +routing calls are processed except those explicitly marked with @ignore. + + + +### Routing structure analysis + +The Ktor compiler plugin analyzes your server routing DSL to determine the structural shape of your API. This analysis +is based solely on route declarations and does not inspect the contents of route handlers. + +The following is automatically inferred from the selectors in the routing API tree: +- Merged paths (for example, `/api/v1/users/{id}`). - HTTP methods (such as `GET` and `POST`). +- Path parameters. ```kotlin routing { @@ -72,72 +105,228 @@ routing { } ``` -Because request parameters and responses are handled inside route lambdas, the plugin cannot infer detailed -request/response schemas automatically. To generate a complete and useful specification, you can use annotations. +Because request parameters, bodies, and responses are handled inside route lambdas, the compiler cannot infer a complete +OpenAPI description from the routing structure alone. To enrich the generated metadata, Ktor supports +[annotations](#annotate-routes) and [automatic inference](#code-inference) based on common request-handling patterns. + +### Code inference + +When code inference is enabled, the compiler plugin recognizes common Ktor usage patterns and generates +equivalent runtime annotations automatically. -## Annotate routes +The following table summarizes the supported inference rules: -To enrich the specification, Ktor uses a KDoc-like annotation API. Annotations provide metadata that cannot be inferred -from code and integrate seamlessly with existing routes. +| Rule | Description | Input | Output (from annotate scope) | +|---------------------|----------------------------------------------------------------|----------------------------------------------------------------------------|--------------------------------------------------------------------------| +| Request Body | Provides request body schema from `ContentNegotiation` reads | `call.receive()` | `requestBody { schema = jsonSchema() }` | +| Response Body | Provides response body schema from `ContentNegotiation` writes | `call.respond()` | `responses { HttpStatusCode.OK { schema = jsonSchema() } }` | +| Response Headers | Includes custom headers for responses | `call.response.header("X-Foo", "Bar")` | `responses { HttpStatusCode.OK { headers { header("X-Foo", "Bar") } } }` | +| Path Parameters | Finds path parameter references | `call.parameters["id"]` | `parameters { path("id") }` | +| Query Parameters | Finds query parameter references | `call.queryParameters["name"]` | `parameters { query("name") }` | +| Request Headers | Finds request header references | `call.request.headers["X-Foo"]` | `parameters { header("X-Foo") }` | +| Resource API routes | Infers call structure for the Resources routing API | `call.get { /**/ }; @Resource("/list") class List(val name: String)` | `parameters { query("name") }` | + +Inference follows extracted functions where possible and attempts to generate consistent documentation for typical +request and response flows. + +#### Disable inference for an endpoint + +If inference produces incorrect metadata for a specific endpoint, you can exclude it by adding an `ignore` marker: ```kotlin -/** - * Get a single user. - * - * @path id The ID of the user - * @response 404 The user was not found - * @response 200 [User] The user. - */ -get("/api/users/{id}") { - val user = repository.get(call.parameters["id"]!!) - ?: return@get call.respond(HttpStatusCode.NotFound) - call.respond(user) +// ignore! +get("/comments") { + // ... } +``` + +## Annotate routes {id="annotate-routes"} + +To enrich the specification, Ktor supports two ways of annotating routes: + +- [Comment-based annotations](#comment-annotations), analyzed by the compiler plugin. +- [Runtime route annotations](#runtime-route-annotations), defined using the `.describe {}` DSL. + +You can use either approach or combine both. +### Comment-based route annotations {id="comment-annotations"} + +Comment-based annotations provide metadata that cannot be inferred from code and integrate seamlessly with existing +routes. + +Metadata is defined by placing a keyword at the start of a line, followed by a colon (`:`) and its value. + +You can attach comments directly to route declarations: + +```kotlin ``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="79-95"} -### Supported KDoc fields +#### Formatting rules -| Tag | Format | Description | -|-----------------|-------------------------------------------------|-------------------------------------------------| -| `@tag` | `@tag *name` | Associates the endpoint with a tag for grouping | -| `@path` | `@path [Type] name description` | Describes a path parameter | -| `@query` | `@query [Type] name description` | Query parameter | -| `@header` | `@header [Type] name description` | Header parameter | -| `@cookie` | `@cookie [Type] name description` | Cookie parameter | -| `@body` | `@body contentType [Type] description` | Request body | -| `@response` | `@response code contentType [Type] description` | Response with optional type | -| `@deprecated` | `@deprecated reason` | Marks endpoint deprecated | -| `@description` | `@description text` | Extended description | -| `@security` | `@security scheme` | Security requirements | -| `@externalDocs` | `@external href` | External documentation links | +- Keywords must appear at the start of the line. +- A colon (`:`) separates the keyword from its value. +- Plural forms (for example, `Tags`, `Responses`) allow grouped definitions. +- Singular forms (for example, `Tag`, `Response`) are also supported. +- Top-level bullet points (`-`) are optional and only affect formatting. +The following variants are equivalent: -## Generate the specification +```kotlin +/** + * Tag: widgets + * + * Tags: + * - widgets + * + * - Tags: + * - widgets + */ +``` -To generate the OpenAPI specification, run the following Gradle task: +#### Supported comment fields -```shell -./gradlew buildOpenApi +| Tag | Format | Description | +|----------------|-------------------------------------------------|----------------------------------| +| `Tag` | `Tag: name` | Groups the endpoint under a tag | +| `Path` | `Path: [Type] name description` | Path parameter | +| `Query` | `Query: [Type] name description` | Query parameter | +| `Header` | `Header: [Type] name description` | Header parameter | +| `Cookie` | `Cookie: [Type] name description` | Cookie parameter | +| `Body` | `Body: contentType [Type] description` | Request body | +| `Response` | `Response: code contentType [Type] description` | Response definition | +| `Deprecated` | `Deprecated: reason` | Marks the endpoint as deprecated | +| `Description` | `Description: text` | Extended description | +| `Security` | `Security: scheme` | Security requirements | +| `ExternalDocs` | `ExternalDocs: href` | External documentation link | + + +### Runtime route annotations {id="runtime-route-annotations"} + + + +In cases where compile-time analysis is insufficient, such as when using dynamic routing, interceptors, or conditional +logic, you can attach OpenAPI operation metadata directly to a route at runtime using the `.describe {}` extension +function. + +Each annotated route defines an OpenAPI [Operation object](https://swagger.io/specification/#operation-object), +which represents a single HTTP operation (for example, `GET /users`) in the generated OpenAPI specification. +The metadata is attached to the routing tree at runtime and is consumed by the OpenAPI and Swagger UI plugins. + +The `.describe {}` DSL maps directly to the OpenAPI specification. Property names and structure correspond to the +fields defined for an Operation object, including parameters, request bodies, responses, security requirements, +servers, callbacks, and specification extensions (`x-*`). + +The runtime route annotations API is experimental and requires opting in using `@OptIn(ExperimentalKtorApi::class)`: + +```kotlin ``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="102-131"} + +> For a complete list of available fields, refer to the [OpenAPI specification](https://swagger.io/specification/#operation-object). +> +{style="tip"} + +Runtime annotations are merged with compiler-generated and comment-based metadata. +When the same OpenAPI field is defined by multiple sources, values provided by runtime annotations take [precedence](#metadata-precedence). + +## Hide routes from the OpenAPI specification + +To exclude a route and its children from the generated OpenAPI document, use the `Route.hide()` function: + +```kotlin +@OptIn(ExperimentalKtorApi::class) +get("/routes") { + // .... +}.hide() +``` + +This is useful for internal, administrative, or diagnostic endpoints that should not be published, including routes used +to [generate the OpenAPI specification](#assemble-and-serve-the-specification) itself. -This task runs the Kotlin compiler with a custom plugin that analyzes your routing code and produces a -JSON specification. +The OpenAPI and Swagger UI plugins call `.hide()` automatically, so their routes are excluded from the resulting +document. -> Some constructs cannot be evaluated at compile time. The generated specification may be incomplete. Improvements are -> planned for later Ktor releases. +## Generate and serve the specification + +The OpenAPI specification is assembled at runtime from runtime route annotations and metadata generated by the compiler +plugin. + +You can expose the specification in the following ways: + +- [Assemble and serve the OpenAPI document manually](#assemble-and-serve-the-specification). +- Use the [OpenAPI](server-openapi.md) or [SwaggerUI](server-swagger-ui.md) plugins to serve the specification and +interactive documentation. + +### Assemble and serve the specification + +To assemble a complete OpenAPI document at runtime, create an `OpenApiDoc` instance and provide the routes that should +be included in the specification. + +The document is assembled from compiler-generated metadata and runtime route annotations from the routing tree. The +resulting `OpenApiDoc` instance always reflects the current state of the application. + +You typically construct the document from a route handler and respond with it directly: + +```kotlin +``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="43-46"} + +In this example, the OpenAPI document is serialized using the [`ContentNegotiation`](server-serialization.md) plugin. +This assumes that a JSON serializer (for example, `kotlinx.serialization`) is installed. + +No additional build or generation step is required. Changes to routes or annotations are reflected automatically the +next time the specification is requested. + +> If you want to make serialization explicit or avoid relying on `ContentNegotiation`, you can encode the document +> manually and respond with a JSON: +> +> ```kotlin +> call.respondText( +> Json.encodeToString(docs), +> ContentType.Application.Json +> ) +>``` > {style="note"} -## Serve the specification +### Serve interactive documentation -To make the generated specification available at runtime, you can use the [OpenAPI](server-openapi.md) -or [SwaggerUI](server-swagger-ui.md) plugins. +To expose the OpenAPI specification through an interactive UI, use the [OpenAPI](server-openapi.md) +and [Swagger UI](server-swagger-ui.md) plugins. -The following example serves the generated specification file at an OpenAPI endpoint: +Both plugins assemble the specification at runtime and can read metadata directly from the routing tree. +They differ in how the documentation is rendered: +- The OpenAPI plugin renders documentation on the server and serves pre-generated HTML. +- The Swagger UI plugin serves the OpenAPI specification as JSON or YAML and renders the UI in the browser using +Swagger UI. ```kotlin -routing { - openAPI("/docs", swaggerFile = "openapi/generated.json") +// Serves the OpenAPI UI +openAPI("/openApi") + +// Serves the Swagger UI +swaggerUI("/swaggerUI") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.RoutingSource(ContentType.Application.Json) { + apiRoute.descendants() + } } -``` \ No newline at end of file +``` + +### Metadata precedence + +The final OpenAPI specification is assembled at runtime by merging metadata contributed from multiple sources. + +The following sources are applied, in order: + +1. Compiler-generated metadata, including: + - [Routing structure analysis](#routing-structure-analysis) + - [Code inference](#code-inference) +2. [Comment-based route annotations](#comment-annotations) +3. [Runtime route annotations](#runtime-route-annotations) + +When the same OpenAPI field is defined by multiple sources, values provided by runtime annotations +take precedence over comment-based annotations and compiler-generated metadata. + +Metadata that is not explicitly overridden is preserved and merged into the final document. \ No newline at end of file diff --git a/topics/server-openapi.md b/topics/server-openapi.md index 77a2be060..6a734bd80 100644 --- a/topics/server-openapi.md +++ b/topics/server-openapi.md @@ -19,9 +19,14 @@ The OpenAPI plugin allows you to generate OpenAPI documentation for your project.
-Ktor allows you to generate and serve OpenAPI documentation for your project based on an existing OpenAPI specification. -You can serve an existing YAML or JSON specification, or generate one using the -[OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. +Ktor allows you to serve OpenAPI documentation based on an OpenAPI specification. + +You can provide the OpenAPI specification in one of the following ways: + +* [Serve an existing YAML or JSON file](#static-openapi-file). +* [Generate the specification at runtime using the OpenAPI compiler extension and runtime APIs](#generate-runtime-openapi-metadata). + +In both cases, the OpenAPI plugin assembles the specification on the server and renders the documentation as HTML. ## Add dependencies {id="add_dependencies"} @@ -40,10 +45,13 @@ You can serve an existing YAML or JSON specification, or generate one using the You can replace `$swagger_codegen_version` with the required version of the `swagger-codegen-generators` artifact, for example, `%swagger_codegen_version%`. -## Configure OpenAPI {id="configure-swagger"} +## Use a static OpenAPI file {id="static-openapi-file"} + +To serve OpenAPI documentation from an existing specification, use the [`openAPI()`](%plugin_api_link%) function with +a provided path to the OpenAPI document. -To serve OpenAPI documentation, you need to call the [openAPI](%plugin_api_link%) method that creates a `GET` endpoint with documentation -at the `path` rendered from the OpenAPI specification placed at `swaggerFile`: +The following example creates a `GET` endpoint at the `openapi` path and renders the Swagger UI from the provided +OpenAPI specification file: ```kotlin import io.ktor.server.plugins.openapi.* @@ -54,14 +62,37 @@ routing { } ``` -This method tries to look up the OpenAPI specification in the application resources. -Otherwise, it tries to read the OpenAPI specification from the file system using `java.io.File`. +The plugin first looks for the specification in the application resources. If not found, it attempts to load it from +the file system using `java.io.File`. -By default, the documentation is generated using `StaticHtml2Codegen`. -You can customize generation settings inside the `openAPI` block: +## Generate runtime OpenAPI metadata + +Instead of relying on a static file, you can generate the OpenAPI specification at runtime using metadata produced +by the OpenAPI compiler plugin and route annotations. + +In this mode, the OpenAPI plugin assembles the specification directly from the routing tree: ```kotlin + openAPI(path = "openapi") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing { + routingRoot.descendants() + } +} ``` -{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,56-58,59"} -You can now [run](server-run.md) the application and open the `/openapi` page to see the generated documentation. +With this, you can access the generated OpenAPI documentation at the `/openapi` path, reflecting the current state of the +application. + +> For more information on the OpenAPI compiler extension and runtime APIs, see [](openapi-spec-generation.md). +> +{style="tip"} + +## Configure OpenAPI {id="configure-openapi"} + +By default, documentation is rendered using `StaticHtml2Codegen`. You can customize the renderer inside the `openAPI {}` +block: + +```kotlin +``` +{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,56-58,59"} diff --git a/topics/server-swagger-ui.md b/topics/server-swagger-ui.md index f38fbc75e..11472fe73 100644 --- a/topics/server-swagger-ui.md +++ b/topics/server-swagger-ui.md @@ -19,9 +19,13 @@ The SwaggerUI plugin allows you to generate Swagger UI for your project. -Ktor allows you to generate and serve Swagger UI for your project based on the existing OpenAPI specification. -With Swagger UI, you can visualize and interact with the API resources. You can serve an existing YAML or JSON -specification, or generate one using the [OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. +Ktor allows you to generate and serve Swagger UI for your project based on an OpenAPI specification. +With Swagger UI, you can visualize and interact with your API endpoints directly from the browser. + +You can provide the OpenAPI specification in one of the following ways: + +* [Serve an existing YAML or JSON file](#static-openapi-file). +* [Generate the specification at runtime using the OpenAPI compiler extension and runtime APIs](#generate-runtime-openapi-metadata). ## Add dependencies {id="add_dependencies"} @@ -30,10 +34,13 @@ Serving Swagger UI requires adding the `%artifact_name%` artifact in the build s -## Configure Swagger UI {id="configure-swagger"} +## Use a static OpenAPI file {id="static-openapi-file"} -To serve Swagger UI, you need to call the [swaggerUI](%plugin_api_link%) method that creates a `GET` endpoint with Swagger UI -at the `path` rendered from the OpenAPI specification placed at `swaggerFile`: +To serve Swagger UI from an existing OpenAPI specification file, use the [`swaggerUI()`](%plugin_api_link%) function and +specify the file location. + +The following example creates a `GET` endpoint at the `swagger` path and renders the Swagger UI from the provided +OpenAPI specification file: ```kotlin import io.ktor.server.plugins.swagger.* @@ -44,25 +51,46 @@ routing { } ``` -This method tries to look up the OpenAPI specification in the application resources. -Otherwise, it tries to read the OpenAPI specification from the file system using `java.io.File`. +The plugin first looks for the specification in the application resources. If not found, it attempts to load it from +the file system using `java.io.File`. + +## Generate runtime OpenAPI metadata -Optionally, you can customize Swagger UI inside the `swaggerUI` block. -For example, you can use another Swagger UI version or apply a custom style. +Instead of relying on a static file, you can generate the OpenAPI specification at runtime using metadata produced +by the OpenAPI compiler plugin and route annotations: ```kotlin +swaggerUI("/swaggerUI") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + routingRoot.descendants() + } +} ``` -{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,53-55,59"} -You can now [run](server-run.md) the application and open the `/swagger` page to see the available endpoints, and test them. +With this, you can access the generated OpenAPI documentation at the `/swaggerUI` path, reflecting the current state of +the application. + +> For more information on the OpenAPI compiler extension and runtime APIs, see [](openapi-spec-generation.md). +> +{style="tip"} +## Configure Swagger UI + +You can customize Swagger UI within the `swaggerUI {}` block, for example, by specifying a custom Swagger UI version: + +```kotlin +``` +{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,53-55,59"} ## Configure CORS {id="configure-cors"} -To make sure your API works nicely with Swagger UI, you need to set up a policy for [Cross-Origin Resource Sharing (CORS)](server-cors.md). +To ensure Swagger UI can access your API endpoints correctly, you need to first configure [Cross-Origin Resource Sharing +(CORS)](server-cors.md). + The example below applies the following CORS configuration: -- `anyHost` enables cross-origin requests from any host; -- `allowHeader` allows the `Content-Type` client header used in [content negotiation](server-serialization.md). +* `anyHost` enables cross-origin requests from any host. +* `allowHeader` allows the `Content-Type` client header used for [content negotiation](server-serialization.md). ```kotlin ``` diff --git a/topics/whats-new-330.md b/topics/whats-new-330.md index c23e15d57..90eef8e96 100644 --- a/topics/whats-new-330.md +++ b/topics/whats-new-330.md @@ -176,7 +176,7 @@ This change unifies SSE handling across all client engines and addresses the lim ## Gradle plugin ### OpenAPI specification generation {id="openapi-spec-gen"} - + Ktor 3.3.0 introduces an experimental OpenAPI generation feature via the Gradle plugin and a compiler plugin. This allows you to generate OpenAPI specifications directly from your application code at build time. @@ -189,9 +189,6 @@ It provides the following capabilities: - Security, descriptions, deprecations, and external documentation links - Infer request and response bodies from `call.receive()` and `call.respond()`. - - - #### Generate the OpenAPI specification To generate the OpenAPI specification file from your Ktor routes and KDoc annotations, use the following command: diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index 3e99b1533..15d5d517a 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -151,6 +151,52 @@ routing { } ``` +### Runtime route annotations + + + +Ktor 3.4.0 introduces the `ktor-server-routing-openapi` module, which allows you to attach OpenAPI metadata directly +to routes using runtime annotations. These annotations are applied to routes at runtime and become part of the routing tree, making them available to +OpenAPI-related tooling. + +The API is experimental and requires opting in using `@OptIn(ExperimentalKtorApi::class)`. + +To add metadata to a route at runtime, use the `.describe {}` extension function: + +```kotlin +@OptIn(ExperimentalKtorApi::class) +get("/messages") { + val query = call.parameters["q"]?.let(::parseQuery) + call.respond(messageRepository.getMessages(query)) +}.describe { + parameters { + query("q") { + description = "An encoded query" + required = false + } + } + responses { + HttpStatusCode.OK { + description = "A list of messages" + schema = jsonSchema>() + extension("x-sample-message", testMessage) + } + HttpStatusCode.BadRequest { + description = "Invalid query" + ContentType.Text.Plain() + } + } + summary = "get messages" + description = "Retrieves a list of messages." +} +``` + +You can use this API as a standalone extension or in combination with Ktor's OpenAPI compiler plugin to automatically +generate these calls. The [OpenAPI](server-openapi.md) and +[SwaggerUI](server-swagger-ui.md) plugins also read this metadata when building the OpenAPI specification. + +For more details and examples, see [](openapi-spec-generation.md#runtime-route-annotations). + ### API Key authentication The new [API Key authentication plugin](server-api-key-auth.md) allows you to secure server routes using a shared secret @@ -484,5 +530,58 @@ println("A file saved to ${file.path}") ``` +## Gradle plugin + +### OpenAPI compiler extension + +Previously, the OpenAPI compiler plugin generated a complete, static OpenAPI document at build time. In Ktor 3.4.0, it +instead generates code that provides OpenAPI metadata at runtime, which is consumed by the [OpenAPI](server-openapi.md) +and [Swagger UI](server-swagger-ui.md) plugins when serving the specification. + +The dedicated `buildOpenApi` Gradle task has been removed. The compiler plugin is now automatically applied during +regular builds, and changes to routes or annotations are reflected in the running server without requiring any +additional generation steps. +#### Configuration +Configuration is still done using the `openApi {}` block inside the `ktor` Gradle extension. However, properties used +to define global OpenAPI metadata, such as `title`, `version`, `description`, and `target`, have been deprecated and are +ignored. + +Global OpenAPI metadata is now defined and resolved at runtime rather than during compilation. + +The compiler extension configuration is now limited to feature options that control how metadata is inferred and +collected. + +For users migrating from the experimental preview in Ktor 3.3.0, the configuration has changed as follows: + + + +```kotlin +// build.gradle.kts +ktor { + @OptIn(OpenApiPreview::class) + openApi { + target = project.layout.projectDirectory.file("api.json") + title = "OpenAPI example" + version = "2.1" + summary = "This is a sample API" + } +} +``` + +```kotlin +// build.gradle.kts +ktor { + openApi { + // Global control for the compiler plugin + enabled = true + // Enables and disables inferring details from call handler code + codeInferenceEnabled = true + // Toggles whether all routes should be analysed or only commented ones + onlyCommented = false + } +} +``` + + From c7d619c76bfaa63d42d2d254b1d1ddf9693e9bf5 Mon Sep 17 00:00:00 2001 From: Pantus Oleh <53688419+zibet27@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:48:11 +0100 Subject: [PATCH 23/27] KTOR-8996 Add documentation for WebRTC iOS Client (#751) * KTOR-8996 Add documentation for WebRTC iOS Client --- topics/client-webrtc.md | 120 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/topics/client-webrtc.md b/topics/client-webrtc.md index 349e36e3c..850a37f8b 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