Releases

Ktor 2.2.1 (and 2.2.0) released


Important: Yesterday we released 2.2.0 to Maven Central, but immediately after we noticed a critical issue of AtomicFu that needed addressing, and as a consequence shortly after we pushed 2.2.1. As such, if you are using 2.2.0, we strongly advise moving to 2.2.1.

We’ve released Ktor 2.2.0/2.2.1, which introduces many new features and fixes. Moreover, with the latest 2022.3 IntelliJ IDEA version, the Ktor plugin lets you generate OpenAPI documentation for server-side Ktor applications using the dedicated intention action.

For guidance about migrating your Ktor project to the new 2.2.0 version, see these instructions.
If you’re new to Ktor, create your first application today!

Generating OpenAPI

To generate an OpenAPI specification for a Ktor application, place the caret at the embeddedServer function call, press Alt+Enter, and choose Generate OpenAPI documentation for current module. IntelliJ IDEA will generate the paths for all application endpoints defined using either string path patterns or type-safe routes.

You can learn more about this feature from the documentation: Ktor | OpenAPI.

Note: The following 2 known issues in the OpenAPI generator will be fixed in the next IntelliJ IDEA patch release: KTOR-5194 and KTOR-5195.

Server

Serving OpenAPI

Ktor 2.2.0 allows you to generate and serve Swagger UI for your project based on the existing OpenAPI specification. For example, if your project contains the OpenAPI specification at resources/openapi/documentation.yaml, you can serve Swagger UI for this specification by calling swaggerUI with the following parameters:

routing {
    swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml")
}

Then you can run the application, open the http://0.0.0.0:8080/swagger page, and interact with the API resources using the generated Swagger UI.

To learn more about how to set up this plugin, see the Swagger UI topic. Similarly, you can render a web page with OpenAPI documentation using the OpenAPI plugin.

Rate limiting

In this release, we’ve added a new RateLimit plugin, which allows you to set rate limiting for incoming requests. For example, you can enable rate limiting globally as shown below:

install(RateLimit) {
    global {
        rateLimiter(limit = 5, refillPeriod = 60.seconds)
    }
}

You can also register a rate-limiting provider with the specified name:

install(RateLimit) {
    register(RateLimitName("protected")) {
        rateLimiter(limit = 30, refillPeriod = 60.seconds)
    }
}

And you can apply the provider to a specific route:

routing {
    rateLimit(RateLimitName("protected")) {
        get("/protected-api") {
            val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
            call.respondText("Welcome to protected API! $requestsLeft requests left.")
        }
    }
}

You can learn more from the Rate limiting help topic.

Authentication

Bearer authentication

As you might know, the Ktor server already supports the Bearer authentication scheme as part of OAuth or JWT. With this release, you can also provide custom logic for authorizing bearer tokens using the dedicated bearer authentication provider, which allows you to check the token sent by the client:

install(Authentication) {
    bearer("auth-bearer") {
        realm = "Access to the '/' path"
        authenticate { tokenCredential ->
            if (tokenCredential.token == "abc123") {
                UserIdPrincipal("jetbrains")
            } else {
                null
            }
        }
    }
}

We’d like to thank our community contributor oharaandrew314 for their contribution in this area.

For more details, see the Bearer authentication help topic.

Nested authentication improvements

The Authentication plugin allows you to define the authorization scope for resources by using the authenticate function. In older versions of Ktor, if you had several nested calls of the authenticate method for different providers, the client had to provide authentication data for any provider. With v2.2.0, you can define a strategy for resolving nested authentication providers. This might be useful if the client must provide authentication data for all registered providers. In the code snippet below, only a user that passes session authentication can try to access the /admin route using basic authentication:

routing {
    authenticate("auth-session", strategy = AuthenticationStrategy.Required) {
        get("/hello") {
            // ...
        }
        authenticate("auth-basic", strategy = AuthenticationStrategy.Required) {
            get("/admin") {
                // ...
            }
        }
    }
}

We are planning to make the AuthenticationStrategy.Required strategy the default in the future to make the behavior of nested authentication providers clearer.

Digest authentication

We’ve added the ability to map the credentials to a custom principal for the Digest provider. In the example below, validate returns a CustomPrincipal instance in case of successful authentication.

fun Application.main() {
    install(Authentication) {
        digest("auth-digest") {
            // ...
            validate { credentials ->
                if (credentials.userName.isNotEmpty()) {
                    CustomPrincipal(credentials.userName, credentials.realm)
                } else {
                    null
                }
            }
        }
    }
}

Serialization

With this release, we’ve added the ProtoBuf serializer for serializing/deserializing data objects. To start using ProtoBuf, add the ktor-serialization-kotlinx-protobuf dependency to the build script and install the serializer as follows:

import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.protobuf.*

install(ContentNegotiation) {
    protobuf()
}

You can also use the ProtoBuf serializer with WebSockets:

import kotlinx.serialization.protobuf.*

install(WebSockets) {
    contentConverter = KotlinxWebsocketSerializationConverter(ProtoBuf)
}

Custom plugins configuration

Ktor 2.2.0 lets you configure custom plugins in a configuration file. For example, the application.yaml configuration file contains a configuration for a custom plugin in the http.custom_header group:

http:
  custom_header:
    header_name: X-Another-Custom-Header
    header_value: Some value

You can access the configuration values as follows:

class CustomHeaderConfiguration(config: ApplicationConfig) {
    var headerName: String = config.tryGetString("header_name") ?: "Custom-Header-Name"
    var headerValue: String = config.tryGetString("header_value") ?: "Default value"
}

Finally, set the configurationPath parameter to http.custom_header when creating a plugin:

val CustomHeaderPluginConfigurable = createApplicationPlugin(
    name = "CustomHeaderPluginConfigurable",
    configurationPath = "http.custom_header",
    createConfiguration = ::CustomHeaderConfiguration
) {
    // …
}

For the complete code example, visit the Configuration in a file help section.

Native server

We’ve made the following improvements to the Native server in this release:

  • The Native server now supports the Sessions plugin.
  • You can change the logging level on the Native server by using the KTOR_LOG_LEVEL environment variable. Enabling route tracing in this way can help you understand why some routes are not being executed.

TLS and certificates

Generating certificates

With this release, the buildKeyStore method provides more flexibility for generating self-signed certificates. For instance, you can now specify which domains are allowed in a test certificate:

val keyStore = buildKeyStore {
    certificate("sampleAlias") {
        password = "foobar"
        domains = listOf("127.0.0.1", "0.0.0.0", "localhost")
    }
}

We’d like to thank joffrey-bion for contributing to this improvement.

Configuring TLS versions

The enabledProtocols property available inside sslConnector now allows you to block the use of TLS 1.0 and 1.1.

val environment = applicationEngineEnvironment {
    // ...
    sslConnector(
        // ...
        enabledProtocols = listOf("TLSv1.3", "TLSv1.2")
    }
}

We’d like to thank joreilly for contributing to this improvement.

Obtaining connection details

This release also brings improvements to resolving connection details such as hostname and port. For example, before v2.2.0, you could get the hostname or port to which the request was made using the RequestConnectionPoint.host and RequestConnectionPoint.port properties:

get("/hello") {
    val originHost = call.request.origin.host
    val originPort = call.request.origin.port
}

With this release, these properties have been deprecated and replaced with serverHost and serverPort. We’ve also added the localHost/localPort properties that return the hostname or port on which the request was received. Refer to the Original request information section to learn more.

Other improvements

This release also adds other improvements for TLS and certificates, including test server lifecycle management, logging debug information for routing and other plugins, and other bug fixes and features.

Client

New custom plugins API

In addition to simplifying the extensibility API for the Ktor server, we’ve implemented a new API for creating custom client plugins. Instead of intercepting pipelines, you can now access different stages of handling requests and responses by using a set of handlers, such as onRequest, onResponse, and so on.

The code snippet below shows how to create a plugin that adds a predefined custom header to each request:

import io.ktor.client.plugins.api.*

val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") {
    onRequest { request, _ ->
        request.headers.append("X-Custom-Header", "Default value")
    }
}

To learn more, see the Custom plugins topic in our documentation.

Persistent HttpCache

We’ve implemented the ability to implement persistent caching for the Ktor client. To do this, you can use the CacheStorage interface or its FileStorage implementation on JVM. The code snippet below shows how to do the latter:

val client = HttpClient(CIO) {
    install(HttpCache) {
        val cacheFile = Files.createDirectories(Paths.get("build/cache")).toFile()
        publicStorage(FileStorage(cacheFile))
    }
}

Note that the publicStorage and privateStorageproperties have been deprecated in favor of the respective methods.

Serialization

The ProtoBuf serializer is also available for the Ktor client. The following snippet shows how to enable ProtoBuf for the ContentNegotiation plugin:

import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.protobuf.*

val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        protobuf()
    }
}

The serializer is also available for WebSockets.

Native windows HTTP client

This release adds the WinHttp native engine that targets Windows-based operating systems and supports WebSockets. To learn how to configure it, see the WinHttp doc section.

Sample updates

As you might know, the ktor-samples repository contains a collection of ready-to-use samples for Ktor. We’ve migrated these samples to the latest version of Ktor API, in particular, replaced the Location plugin usages to Resources and updated tests to a new testing API implemented in v2.0.0. You can browse through these samples to understand different ways to accomplish things in Ktor.

Docs infrastructure

The final thing we want to mention is the latest changes we’ve made to our documentation infrastructure. Ktor documentation lives in a separate GitHub repository: https://github.com/ktorio/ktor-documentation.

To build our documentation, we now use Writerside, a new tool from JetBrains for creating and publishing docs. It uses the docs-as-code approach and provides automatic quality checks, natural language assistance, live preview, and other useful features. If you want to try this tool, feel free to enroll in the Writerside Early Access Program at https://jb.gg/writerside.

Happy developing!
The Ktor team