What's new in Ktor 3.4.0
Ktor 3.4.0 delivers a wide range of enhancements across server, client, and tooling. Here are the highlights for this feature release:
Ktor Server
OAuth fallback for error handling
Ktor 3.4.0 introduces a new fallback() function for the OAuth 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:
Zstd compression support
Zstd compression is now supported by the Compression 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, add the ktor-server-compression-zstd dependency to your project:
Then, call the zstd() function inside the install(Compression) {} block with your desired configuration:
SSL trust store settings in a configuration file
Ktor now allows you to configure additional SSL settings 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:
From the code above:
trustStore– the path to the trust store file containing trusted certificates.trustStorePassword– password for the trust store.enabledProtocols– a list of allowed TLS protocols.
HTML fragments for partial responses
Ktor now provides a new .respondHtmlFragment() function for sending partial HTML responses. This is useful when generating markup that does not require a full <html> document, such as dynamic UI updates with tools like HTMX.
The new API is part of the HTML DSL plugin and allows you to return HTML rooted in any element:
HTTP request lifecycle
The new HttpRequestLifecycle plugin allows you to cancel inflight HTTP requests when the client disconnects. This is useful when you need to cancel an inflight HTTP request for a long-running or resource-intensive request when the client disconnects.
Enable this feature by installing the HttpRequestLifecycle plugin and setting cancelCallOnClose = true:
When the client disconnects, the coroutine handling the request is canceled, and structured concurrency handles cleaning all resources. Any launch or async coroutines started by the request are also canceled. This is currently only supported for the Netty and CIO engine.
New method to respond with a resource
The new call.respondResource() method works in a similar way to call.respondFile(), 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:
Runtime OpenAPI 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:
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 and SwaggerUI plugins also read this metadata when building the OpenAPI specification.
For more details and examples, see Runtime route annotations.
API Key authentication
The new API Key authentication plugin 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 and lets you validate incoming API keys using custom logic, customize the header name, and protect specific routes with standard authenticate blocks:
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 API Key authentication.
Core
Multiple header parsing
The new Headers.getSplitValues() function simplifies working with headers that contain multiple values in a single line.
The getSplitValues() function returns all values for the given header and splits them using the specified separator (, by default):
By default, separators inside double-quoted strings are ignored, but you can change this by setting splitInsideQuotes = true:
Ktor Client
Authentication token cache control
Prior to Ktor 3.4.0, applications using Basic and Bearer authentication providers could continue sending outdated tokens or credentials after a user logged out or updated their authentication data. This happened because each provider internally caches the result of the loadTokens() function through an internal component responsible for storing loaded authentication tokens, and this cache remained active until manually cleared.
Ktor 3.4.0 introduces new functions and configuration options that give you explicit and convenient control over token caching behavior.
Accessing and clearing authentication tokens
You can now access authentication providers directly from the client and clear their cached tokens when needed.
To clear the token for a specific provider, use the .clearToken() function:
Retrieve all authentication providers:
To clear cached tokens from all providers that support token clearing (currently Basic and Bearer), use the HttpClient.clearAuthTokens() function:
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:
Disabling caching is especially useful when authentication data changes frequently or must always reflect the most recent state.
Duplex streaming for OkHttp
The OkHttp client engine now supports duplex streaming, enabling clients to send request body data and receive response data simultaneously.
Unlike regular HTTP calls where the request body must be fully sent before the response begins, duplex mode supports bidirectional streaming, allowing the client to send and receive data concurrently.
Duplex streaming is available for HTTP/2 connections and can be enabled using the new duplexStreamingEnabled property in OkHttpConfig:
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.
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:
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:
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.
In the above example, if ContentNegotiation is already installed, its configuration is replaced with the new one provided in the block.
Replace default request configuration
The defaultRequest() function now accepts an optional replace parameter (default is false). When set to true, the new configuration replaces any previously defined default request settings instead of merging with them.
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 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:
You can then use APIs available to both js and wasmJs targets:
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:
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 and Swagger UI 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: