Migrating from 1.6.x to 2.0.x
Edit pageLast modified: 02 April 2024This guide provides instructions on how to migrate your Ktor application from the 1.6.x version to 2.0.x.
Ktor Server
Server code is moved to the 'io.ktor.server.*' package
To unify and better distinguish the server and client APIs, server code is moved to the io.ktor.server.*
package (KTOR-2865). This means that you need to update dependencies for and imports in your application, as shown below.
Dependencies
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
tip
To add all plugins at once, you can use the
io.ktor:ktor-server
artifact.
Imports
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
WebSockets code is moved to the 'websockets' package
WebSockets code is moved from http-cio
to the websockets
package. This requires updating imports as follows:
1.6.x | 2.0.0 |
---|---|
|
|
Note that this change also affects the client.
Feature is renamed to Plugin
In Ktor 2.0.0, Feature is renamed to Plugin to better describe functionality that intercepts the request/response pipeline (KTOR-2326). This affects the entire Ktor API and requires updating your application as described below.
Imports
Installing any plugin requires updating imports and also depends on moving server code to the io.ktor.server.*
package:
1.6.x | 2.0.0 |
---|---|
|
|
Custom plugins
Renaming Feature to Plugin introduces the following changes for API related to custom plugins:
The
ApplicationFeature
interface is renamed toBaseApplicationPlugin
.The
Features
pipeline phase is renamed toPlugins
.
tip
Note that starting with v2.0.0, Ktor provides the new API for creating custom plugins. In general, this API doesn't require an understanding of internal Ktor concepts, such as pipelines, phases, and so on. Instead, you have access to different stages of handling requests and responses using various handlers, such as
onCall
,onCallReceive
,onCallRespond
, and so on. You can learn how pipeline phases map to handlers in a new API from this section: Mapping of pipeline phases to new API handlers.
Content negotiation and serialization
Content negotiation and serialization server API was refactored to reuse serialization libraries between the server and client. The main changes are:
ContentNegotiation
is moved fromktor-server-core
to a separatektor-server-content-negotiation
artifact.Serialization libraries are moved from
ktor-*
to thektor-serialization-*
artifacts also used by the client.
You need to update dependencies for and imports in your application, as shown below.
Dependencies
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
|
| |
|
| |
|
| |
|
|
Imports
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
|
| |
|
| |
|
|
Custom converters
Signatures of functions exposed by the ContentConverter interface are changed in the following way:
interface ContentConverter {
suspend fun convertForSend(context: PipelineContext<Any, ApplicationCall>, contentType: ContentType, value: Any): Any?
suspend fun convertForReceive(context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): Any?
}
interface ContentConverter {
suspend fun serialize(contentType: ContentType, charset: Charset, typeInfo: TypeInfo, value: Any): OutgoingContent?
suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any?
}
Testing API
With v2.0.0, the Ktor server uses a new API for testing, which solves various issues described in KTOR-971. The main changes are:
The
withTestApplication
/withApplication
functions are replaced with a newtestApplication
function.Inside the
testApplication
function, you need to use the existing Ktor client instance to make requests to your server and verify the results.To test specific functionalities (for example, cookies or WebSockets), you need to create a new client instance and install a corresponding plugin.
Let's take a look at several examples of migrating 1.6.x tests to 2.0.0:
Basic server test
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())
}
x-www-form-urlencoded
In the test below, the handleRequest
function is replaced with the client.post
request:
@Test
fun testPostLegacyApi() = withTestApplication(Application::main) {
with(handleRequest(HttpMethod.Post, "/signup"){
addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(listOf("username" to "JetBrains", "email" to "example@jetbrains.com", "password" to "foobar", "confirmation" to "foobar").formUrlEncode())
}) {
assertEquals("The 'JetBrains' account is created", response.content)
}
}
@Test
fun testPost() = testApplication {
val response = client.post("/signup") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(listOf("username" to "JetBrains", "email" to "example@jetbrains.com", "password" to "foobar", "confirmation" to "foobar").formUrlEncode())
}
assertEquals("The 'JetBrains' account is created", response.bodyAsText())
}
multipart/form-data
To build multipart/form-data
in v2.0.0, you need to pass MultiPartFormDataContent
to the client's setBody
function:
@Test
fun testUploadLegacyApi() = withTestApplication(Application::main) {
with(handleRequest(HttpMethod.Post, "/upload"){
val boundary = "WebAppBoundary"
val fileBytes = File("ktor_logo.png").readBytes()
addHeader(HttpHeaders.ContentType, ContentType.MultiPart.FormData.withParameter("boundary", boundary).toString())
setBody(boundary, listOf(
PartData.FormItem("Ktor logo", { }, headersOf(
HttpHeaders.ContentDisposition,
ContentDisposition.Inline
.withParameter(ContentDisposition.Parameters.Name, "description")
.toString()
)),
PartData.FileItem({ fileBytes.inputStream().asInput() }, {}, headersOf(
HttpHeaders.ContentDisposition,
ContentDisposition.File
.withParameter(ContentDisposition.Parameters.Name, "image")
.withParameter(ContentDisposition.Parameters.FileName, "ktor_logo.png")
.toString()
))
))
}) {
assertEquals("Ktor logo is uploaded to 'uploads/ktor_logo.png'", response.content)
}
}
@Test
fun testUpload() = testApplication {
val boundary = "WebAppBoundary"
val response = client.post("/upload") {
setBody(
MultiPartFormDataContent(
formData {
append("description", "Ktor logo")
append("image", File("ktor_logo.png").readBytes(), Headers.build {
append(HttpHeaders.ContentType, "image/png")
append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
})
},
boundary,
ContentType.MultiPart.FormData.withParameter("boundary", boundary)
)
)
}
assertEquals("Ktor logo is uploaded to 'uploads/ktor_logo.png'", response.bodyAsText())
}
JSON data
In v.1.6.x, you can serialize JSON data using the Json.encodeToString
function provided by the kotlinx.serialization
library. With v2.0.0, you need to create a new client instance and install the ContentNegotiation plugin that allows serializing/deserializing the content in a specific format:
@Test
fun testPostCustomerLegacyApi() = withTestApplication(Application::main) {
with(handleRequest(HttpMethod.Post, "/customer"){
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(Json.encodeToString(Customer(3, "Jet", "Brains")))
}) {
assertEquals("Customer stored correctly", response.content)
assertEquals(HttpStatusCode.Created, response.status())
}
}
@Test
fun testPostCustomer() = testApplication {
val client = createClient {
install(ContentNegotiation) {
json()
}
}
val response = client.post("/customer") {
contentType(ContentType.Application.Json)
setBody(Customer(3, "Jet", "Brains"))
}
assertEquals("Customer stored correctly", response.bodyAsText())
assertEquals(HttpStatusCode.Created, response.status)
}
Preserve cookies during testing
In v1.6.x, cookiesSession
is used to preserve cookies between requests when testing. With v2.0.0, you need to create a new client instance and install the HttpCookies plugin:
@Test
fun testRequestsLegacyApi() = withTestApplication(Application::main) {
fun doRequestAndCheckResponse(path: String, expected: String) {
handleRequest(HttpMethod.Get, path).apply {
assertEquals(expected, response.content)
}
}
cookiesSession {
handleRequest(HttpMethod.Get, "/login") {}.apply {}
doRequestAndCheckResponse("/user", "Session ID is 123abc. Reload count is 0.")
doRequestAndCheckResponse("/user", "Session ID is 123abc. Reload count is 1.")
doRequestAndCheckResponse("/user", "Session ID is 123abc. Reload count is 2.")
handleRequest(HttpMethod.Get, "/logout").apply {}
doRequestAndCheckResponse("/user", "Session doesn't exist or is expired.")
}
}
@Test
fun testRequests() = testApplication {
val client = createClient {
install(HttpCookies)
}
val loginResponse = client.get("/login")
val response1 = client.get("/user")
assertEquals("Session ID is 123abc. Reload count is 1.", response1.bodyAsText())
val response2 = client.get("/user")
assertEquals("Session ID is 123abc. Reload count is 2.", response2.bodyAsText())
val response3 = client.get("/user")
assertEquals("Session ID is 123abc. Reload count is 3.", response3.bodyAsText())
val logoutResponse = client.get("/logout")
assertEquals("Session doesn't exist or is expired.", logoutResponse.bodyAsText())
}
WebSockets
In the old API, handleWebSocketConversation
is used to test WebSocket conversations. With v2.0.0, you can test WebSocket conversations by using the WebSockets plugin provided by the client:
@Test
fun testConversationLegacyApi() {
withTestApplication(Application::module) {
handleWebSocketConversation("/echo") { incoming, outgoing ->
val greetingText = (incoming.receive() as Frame.Text).readText()
assertEquals("Please enter your name", greetingText)
outgoing.send(Frame.Text("JetBrains"))
val responseText = (incoming.receive() as Frame.Text).readText()
assertEquals("Hi, JetBrains!", responseText)
}
}
}
@Test
fun testConversation() {
testApplication {
val client = createClient {
install(WebSockets)
}
client.webSocket("/echo") {
val greetingText = (incoming.receive() as? Frame.Text)?.readText() ?: ""
assertEquals("Please enter your name", greetingText)
send(Frame.Text("JetBrains"))
val responseText = (incoming.receive() as Frame.Text).readText()
assertEquals("Hi, JetBrains!", responseText)
}
}
}
DoubleReceive
With v2.0.0, the DoubleReceive plugin configuration introduces the cacheRawRequest
property, which is opposite to receiveEntireContent
:
In v1.6.x, the
receiveEntireContent
property is set tofalse
by default.In v2.0.0,
cacheRawRequest
is set totrue
by default. ThereceiveEntireContent
property is removed.
Forwarded headers
In v2.0.0, the ForwardedHeaderSupport
and XForwardedHeaderSupport
plugins are renamed to ForwardedHeaders and XForwardedHeaders
, respectively.
Caching headers
The options function used to define caching options now accepts the ApplicationCall
as a lambda argument in addition to OutgoingContent
:
install(CachingHeaders) {
options { outgoingContent ->
// ...
}
}
install(CachingHeaders) {
options { call, outgoingContent ->
// ...
}
}
Conditional headers
The version function used to define a list of resource versions now accepts the ApplicationCall
as a lambda argument in addition to OutgoingContent
:
install(ConditionalHeaders) {
version { outgoingContent ->
// ...
}
}
install(ConditionalHeaders) {
version { call, outgoingContent ->
// ...
}
}
CORS
Several functions used in CORS configuration are renamed:
host
->allowHost
header
->allowHeader
method
->allowMethod
install(CORS) {
host("0.0.0.0:5000")
header(HttpHeaders.ContentType)
method(HttpMethod.Options)
}
install(CORS) {
allowHost("0.0.0.0:5000")
allowHeader(HttpHeaders.ContentType)
allowMethod(HttpMethod.Options)
}
MicrometerMetrics
In v1.6.x, the baseName
property is used to specify the base name (prefix) of Ktor metrics used for monitoring HTTP requests. By default, it equals to ktor.http.server
. With v2.0.0, baseName
is replaced with metricName
whose default value is ktor.http.server.requests
.
Ktor Client
Requests and responses
In v2.0.0, API used to make requests and receive responses is updated to make it more consistent and discoverable (KTOR-29).
Request functions
Request functions with multiple parameters are deprecated. For example, the port
and path
parameters need to be replaced with a the url
parameter exposed by HttpRequestBuilder:
client.get(port = 8080, path = "/customer/3")
client.get { url(port = 8080, path = "/customer/3") }
The HttpRequestBuilder
also allows you to specify additional request parameters inside the request function lambda.
Request body
The HttpRequestBuilder.body
property used to set the request body is replaced with the HttpRequestBuilder.setBody
function:
client.post("http://localhost:8080/post") {
body = "Body content"
}
client.post("http://localhost:8080/post") {
setBody("Body content")
}
Responses
With v2.0.0, request functions (such as get
, post
, put
, submitForm, and so on) don't accept generic arguments for receiving an object of a specific type. Now all request functions return a HttpResponse
object, which exposes the body
function with a generic argument for receiving a specific type instance. You can also use bodyAsText
or bodyAsChannel
to receive content as a string or channel.
val httpResponse: HttpResponse = client.get("https://ktor.io/")
val stringBody: String = httpResponse.receive()
val byteArrayBody: ByteArray = httpResponse.receive()
val httpResponse: HttpResponse = client.get("https://ktor.io/")
val stringBody: String = httpResponse.body()
val byteArrayBody: ByteArray = httpResponse.body()
With the ContentNegotiation plugin installed, you can receive an arbitrary object as follows:
val customer: Customer = client.get("http://localhost:8080/customer/3")
val customer: Customer = client.get("http://localhost:8080/customer/3").body()
Streaming responses
Due to removing generic arguments from request functions, receiving a streaming response requires separate functions. To achieve this, functions with the prepare
prefix are added, such as prepareGet
or preparePost
:
public suspend fun HttpClient.prepareGet(builder: HttpRequestBuilder): HttpStatement
public suspend fun HttpClient.preparePost(builder: HttpRequestBuilder): HttpStatement
The example below shows how to change your code in this case:
client.get<HttpStatement>("https://ktor.io/").execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.receive()
while (!channel.isClosedForRead) {
// Read data
}
}
client.prepareGet("https://ktor.io/").execute { httpResponse ->
val channel: ByteReadChannel = httpResponse.body()
while (!channel.isClosedForRead) {
// Read data
}
}
You can find the full example here: Streaming data.
Response validation
With v2.0.0, the expectSuccess
property used for response validation is set to false
by default. This requires the following changes in your code:
To enable default validation and throw exceptions for non-2xx responses, set the
expectSuccess
property totrue
.If you handle non-2xx exceptions using
handleResponseExceptionWithRequest
, you also need to enableexpectSuccess
explicitly.
HttpResponseValidator
The handleResponseException function is replaced with handleResponseExceptionWithRequest
, which adds access to HttpRequest
to provide additional information in exceptions:
HttpResponseValidator {
handleResponseException { exception ->
// ...
}
}
HttpResponseValidator {
handleResponseExceptionWithRequest { exception, request ->
// ...
}
}
Content negotiation and serialization
The Ktor client now supports content negotiation and shares serialization libraries with the Ktor server. The main changes are:
JsonFeature
is deprecated in favor ofContentNegotiation
, which can be found in thektor-client-content-negotiation
artifact.Serialization libraries are moved from
ktor-client-*
to thektor-serialization-*
artifacts.
You need to update dependencies for and imports in your client code, as shown below.
Dependencies
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
| n/a |
|
kotlinx.serialization |
|
|
Gson |
|
|
Jackson |
|
|
Imports
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
| n/a |
|
kotlinx.serialization |
|
|
Gson |
|
|
Jackson |
|
|
Bearer authentication
The refreshTokens function now uses the RefreshTokenParams
instance as lambda receiver (this
) instead of the HttpResponse
lambda argument (it
):
bearer {
refreshTokens { // it: HttpResponse
// ...
}
}
bearer {
refreshTokens { // this: RefreshTokenParams
// ...
}
}
RefreshTokenParams
exposes the following properties:
response
to access response parameters;client
to make a request to refresh tokens;oldTokens
to access tokens obtained usingloadTokens
.
HttpSend
The API of the HttpSend plugin is changed as follows:
client[HttpSend].intercept { originalCall, request ->
if (originalCall.something()) {
val newCall = execute(request)
// ...
}
}
client.plugin(HttpSend).intercept { request ->
val originalCall = execute(request)
if (originalCall.something()) {
val newCall = execute(request)
// ...
}
}
Note that with v2.0.0 indexed access is not available for accessing plugins. Use the HttpClient.plugin function instead.
The HttpClient.get(plugin: HttpClientPlugin) function is removed
With the 2.0.0 version, the HttpClient.get
function accepting a client plugin is removed. Use the HttpClient.plugin
function instead.
client.get(HttpSend).intercept { ... }
// or
client[HttpSend].intercept { ... }
client.plugin(HttpSend).intercept { ... }
Feature is renamed to Plugin
As for the Ktor server, Feature is renamed to Plugin in the client API. This might affect your application, as described below.
Imports
Update imports for installing plugins:
Subsystem | 1.6.x | 2.0.0 |
---|---|---|
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
Custom plugins
The HttpClientFeature
interface is renamed to HttpClientPlugin
.
New memory model for Native targets
With v2.0.0, using the Ktor client on Native targets requires enabling the new Kotlin/Native memory model: Enable the new MM.
tip
Starting with v2.2.0, the new Kotlin/Native memory model is enabled by default.
The 'Ios' engine is renamed to 'Darwin'
Given that the Ios
engine targets not only iOS but other operating systems, including macOS, or tvOS, in v2.0.0, it is renamed to Darwin
. This causes the following changes:
The
io.ktor:ktor-client-ios
artifact is renamed toio.ktor:ktor-client-darwin
.To create the
HttpClient
instance, you need to pass theDarwin
class as an argument.The
IosClientEngineConfig
configuration class is renamed toDarwinClientEngineConfig
.
To learn how to configure the Darwin
engine, see the Darwin section.
WebSockets code is moved to the 'websockets' package
WebSockets code is moved from http-cio
to the websockets
package. This requires updating imports as follows:
1.6.x | 2.0.0 |
---|---|
|
|
Default request
The DefaultRequest plugin uses a DefaultRequestBuilder
configuration class instead of HttpRequestBuilder
:
val client = HttpClient(CIO) {
defaultRequest {
// this: HttpRequestBuilder
}
}
val client = HttpClient(CIO) {
defaultRequest {
// this: DefaultRequestBuilder
}
}
Thanks for your feedback!