Ktor 3.1.0 Help

Migrating from 2.2.x to 3.0.x

This guide provides instructions on how to migrate your Ktor application from the 2.2.x version to 3.0.x.

Ktor Server

ApplicationEngine, ApplicationEnvironment, and Application

Several design changes have been introduced to improve configurability and provide a more defined separation between the ApplicationEngine, ApplicationEnvironment and Application instances.

Before v3.0.0, ApplicationEngine managed ApplicationEnvironment, which in turn managed Application.

In the current design, however, Application is responsible for creating, owning, and initiating both ApplicationEngine and ApplicationEnvironment.

This restructuring comes with the following set of breaking changes:

These changes will impact existing code that relies on the previous model.

Renamed classes

Package

2.x.x

3.0.x

io.ktor:ktor-server-core

ApplicationEngineEnvironmentBuilder

ApplicationEnvironmentBuilder

io.ktor:ktor-server-core

applicationEngineEnvironment

applicationEnvironment

start() and stop() methods are removed from ApplicationEngineEnvironment

With the merge of ApplicationEngineEnvironment to ApplicationEnvironment, the start() and stop() methods are now only accessible through ApplicationEngine.

2.x.x

3.0.x

ApplicationEngineEnvironment.start()

ApplicationEngine.start()

ApplicationEngineEnvironment.stop()

ApplicationEngine.stop()

Additionally, in the following table you can see the list of removed properties and their current corresponding ownership:

2.x.x

3.0.x

ApplicationEngineEnvironment.connectors

ApplicationEngine.Configuration.connectors

ApplicationEnvironment.developmentMode

Application.developmentMode

ApplicationEnvironment.monitor

Application.monitor

ApplicationEnvironment.parentCoroutineContext

Application.parentCoroutineContext

ApplicationEnvironment.rootPath

Application.rootPath

The ownership changes can be illustrated through the following example:

import io.ktor.server.application.* import io.ktor.server.cio.* import io.ktor.server.engine.* import org.slf4j.helpers.NOPLogger fun defaultServer(module: Application.() -> Unit) = embeddedServer(CIO, environment = applicationEngineEnvironment { log = NOPLogger.NOP_LOGGER connector { port = 8080 } module(module) } )
import io.ktor.server.application.* import io.ktor.server.cio.* import io.ktor.server.engine.* import org.slf4j.helpers.NOPLogger fun defaultServer(module: Application.() -> Unit) = embeddedServer(CIO, environment = applicationEnvironment { log = NOPLogger.NOP_LOGGER }, configure = { connector { port = 8080 } }, module )

commandLineEnvironment() is removed

The commandLineEnvironment() function, used to create an ApplicationEngineEnvironment instance from command line arguments has been removed in Ktor 3.0.0. Instead, you can use the CommandLineConfig function to parse command-line arguments into a configuration object.

To migrate your application from commandLineEnvironment to CommandLineConfig, replace commandLineEnvironment() with a configure block as shown below.

fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args) { connector { port = 8080 } module { routing { get("/") { call.respondText("Hello, world!") } } } }) { requestReadTimeoutSeconds = 5 responseWriteTimeoutSeconds = 5 }.start(wait = true) }
fun main(args: Array<String>) { embeddedServer( factory = Netty, configure = { val cliConfig = CommandLineConfig(args) takeFrom(cliConfig.engineConfig) loadCommonConfiguration(cliConfig.rootConfig.environment.config) } ) { routing { get("/") { call.respondText("Hello, world!") } } }.start(wait = true) }

For more information on command-line configuration with embeddedServer, see the Configuration in code topic.

Introduction of ServerConfigBuilder

A new entity, ServerConfigBuilder, has been introduced for configuring server properties and replaces the previous configuration mechanism of ApplicationPropertiesBuilder. ServerConfigBuilder is used to build instances of the ServerConfig class, which now holds modules, paths, and environment details previously managed by ApplicationProperties.

The following table summarizes the key changes:

Package

2.x.x

3.0.x

io.ktor:ktor-server-core

ApplicationProperties

ServerConfig

io.ktor:ktor-server-core

ApplciationPropertiesBuilder

ServerConfigBuilder

Additionally, in the embeddedServer() function, the applicationProperties attribute has been renamed to rootConfig to reflect this new configuration approach.

When using embeddedServer(), replace the applicationProperties attribute with rootConfig. Here's an example of using the serverConfig block to configure a server with developmentMode explicitly set to true:

fun main(args: Array<String>) { embeddedServer(Netty, serverConfig { developmentMode = true module(Application::module) }, configure = { connector { port = 12345 } } ).start(wait = true) }

Introduction of EmbeddedServer

The class EmbeddedServer is introduced and used to replace ApplicationEngine as a return type of the embeddedServer() function.

For more details about the model change, see issue KTOR-3857 on YouTrack.

Testing

withTestApplication and withApplication have been removed

The withTestApplication and withApplication functions, previously deprecated in the 2.0.0 release, have now been removed from the ktor-server-test-host package.

Instead, use the testApplication function with the existing Ktor client instance to make requests to your server and verify the results.

In the test below, the handleRequest function is replaced with the client.get request:

@Test fun testRootLegacyApi() { withTestApplication(Application::module) { handleRequest(HttpMethod.Get, "/").apply { assertEquals(HttpStatusCode.OK, response.status()) assertEquals("Hello, world!", response.content) } } }
@Test fun testRoot() = testApplication { val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello, world!", response.bodyAsText()) }

For more information, see Testing in Ktor Server.

TestApplication module loading

TestApplication no longer automatically loads modules from a configuration file ( e.g. application.conf). Instead, you must explicitly load your modules within the testApplication function or load the configuration file manually.

Explicit module loading

To explicitly load modules, use the application function within testApplication. This approach allows you to manually specify which modules to load, providing greater control over your test setup.

import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } }
import com.example.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { configureRouting() } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } }
Load modules from a configuration file

If you want to load modules from a configuration file, use the environment function to specify the configuration file for your test.

@Test fun testHello() = testApplication { environment { config = ApplicationConfig("application-custom.conf") } }

For more information on configuring the test application, see the Testing in Ktor Server section.

CallLogging plugin package has been renamed

The CallLogging plugin package has been renamed due to a typo.

2.x.x

3.0.x

io.ktor.server.plugins.callloging

io.ktor.server.plugins.calllogging

ktor-server-host-common module has been removed

Due to Application requiring knowledge of ApplicationEngine, the contents of ktor-server-host-common module have been merged into ktor-server-core, namely the io.ktor.server.engine package.

Ensure that your dependencies are updated accordingly. In most cases, you can simply remove the ktor-server-host-common dependency.

Locations plugin has been removed

The Locations plugin for the Ktor server has been removed. To create type-safe routing, use the Resources plugin instead. This requires the following changes:

  • Replace the io.ktor:ktor-server-locations artifact with io.ktor:ktor-server-resources.

  • The Resources plugin depends on the Kotlin serialization plugin. To add the serialization plugin, see the

  • kotlinx.serialization setup.

  • Update the plugin import from io.ktor.server.locations.* to io.ktor.server.resources.*.

  • Additionally, import the Resource module from io.ktor.resources.

The following example shows how to implement these changes:

import io.ktor.server.locations.* @Location("/articles") class article(val value: Int) fun Application.module() { install(Locations) routing { get<article> { // Get all articles ... call.respondText("List of articles") } } }
import io.ktor.resources.Resource import io.ktor.server.resources.* @Resource("/articles") class Articles(val value: Int) fun Application.module() { install(Resources) routing { get<Articles> { // Get all articles ... call.respondText("List of articles") } } }

For more information on working with Resources, refer to Type-safe routing.

Replacement of java.time in WebSockets configuration

The WebSockets plugin configuration has been updated to use Kotlin’s Duration for the pingPeriod and timeout properties. This replaces the previous use of java.time.Duration for a more idiomatic Kotlin experience.

To migrate existing code to the new format, use the extension functions and properties from the kotlin.time.Duration class to construct durations. In the following example, Duration.ofSeconds() is replaced with Kotlin’s seconds extension:

import java.time.Duration install(WebSockets) { pingPeriod = Duration.ofSeconds(15) timeout = Duration.ofSeconds(15) //.. }
import kotlin.time.Duration.Companion.seconds install(WebSockets) { pingPeriod = 15.seconds timeout = 15.seconds //.. }

You can use similar Kotlin duration extensions (minutes, hours, etc.) as needed for other duration configurations. For more information, see the Duration documentation.

Server socket .bind() is now suspending

To support asynchronous operations in JS and WasmJS environments, the .bind() function for server sockets in both TCPSocketBuilder and UDPSocketBuilder has been updated to a suspending function. This means any calls to .bind() must now be made within a coroutine.

To migrate, ensure .bind() is only called within a coroutine or suspending function. Here's an example of using runBlocking:

runBlocking { val selectorManager = SelectorManager(Dispatchers.IO) val serverSocket = aSocket(selectorManager).tcp().bind("127.0.0.1", 9002) //... }

For more information on working with sockets, see the Sockets documentation.

Multipart form data

New default limit for binary and file items

In Ktor 3.0.0, a default limit of 50MB has been introduced for receiving binary and file items using ApplicationCall.receiveMultipart(). If a received file or binary item exceeds the 50MB limit, an IOException is thrown.

Override the default limit

If your application previously relied on handling files larger than 50MB without explicit configuration, you will need to update your code to avoid unexpected behaviour.

To override the default limit, pass the formFieldLimit parameter when calling .receiveMultipart():

val multipartData = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)

PartData.FileItem.streamProvider() is deprecated

In previous versions of Ktor, the .streamProvider() function in PartData.FileItem was used to access a file item's content as an InputStream. Starting with Ktor 3.0.0, this function has been deprecated.

To migrate your application, replace .streamProvider() with the .provider() function. The .provider() function returns a ByteReadChannel, which is a coroutine-friendly, non-blocking abstraction for reading streams of bytes incrementally. You can then stream data directly from the channel to the file output, using the .copyTo() or .copyAndClose() methods provided by ByteReadChannel.

In the example the .copyAndClose() method transfers data from the ByteReadChannel to a file's WritableByteChannel.

fun Application.main() { routing { post("/upload") { val multipart = call.receiveMultipart() multipart.forEachPart { partData -> if (partData is PartData.FileItem) { var fileName = partData.originalFileName as String val file = File("uploads/$fileName") file.writeBytes(partData.streamProvider().readBytes()) } // ... } } } }
fun Application.main() { routing { post("/upload") { val multipart = call.receiveMultipart() multipart.forEachPart { partData -> if (partData is PartData.FileItem) { var fileName = partData.originalFileName as String val file = File("uploads/$fileName") partData.provider().copyAndClose(file.writeChannel()) } // ... } } } }

For the full example and more information on working with multipart form data, see Request handling of multipart form data.

Session encryption method update

The encryption method offered by the Sessions plugin has been updated to enhance security.

Specifically, the SessionTransportTransformerEncrypt method, which previously derived the MAC from the decrypted session value, now computes it from the encrypted value.

To ensure compatibility with existing sessions, Ktor has introduced the backwardCompatibleRead property. For current configurations, include the property in the constructor of SessionTransportTransformerEncrypt:

install(Sessions) { cookie<UserSession>("user_session") { // ... transform( SessionTransportTransformerEncrypt( secretEncryptKey, // your encrypt key here secretSignKey, // your sign key here backwardCompatibleRead = true ) ) } }

For more information on session encryption in Ktor, see Sign and encrypt session data.

Ktor Client

Renaming of HttpResponse's content property

Prior to Ktor 3.0.0, the content property of HttpResponse provided a raw ByteReadChannel to the response content as it is read from the network. Starting with Ktor 3.0.0, the content property has been renamed to rawContent to better reflect its purpose.

Shared modules

Attribute keys now require exact type matching

In Ktor 3.0.0, AttributeKey instances are now compared by identity and require exact type matching when storing and retrieving values. This ensures type-safety and prevents unintended behaviour caused by mismatched types.

Previously, it was possible to retrieve an attribute with a different generic type than it was stored with, such as using getOrNull<Any>() to fetch an AttributeKey<Boolean>.

To migrate your application, ensure the retrieval type matches the stored type exactly:

val attrs = Attributes() attrs.put(AttributeKey<Boolean>("key"), true) attrs.getOrNull<Boolean>("key")
Last modified: 07 February 2025