Ktor 3.4.0 Help

Receiving responses

All functions used to make an HTTP request (request, get, post, etc.) allow you to receive a response as an HttpResponse object.

HttpResponse exposes the API required to get a response body in various ways (raw bytes, JSON objects, etc.) and obtain response parameters, such as a status code, content type, and headers. For example, you can receive HttpResponse for a GET request without parameters in the following way:

// Configure request URL

Receive response parameters

The HttpResponse class allows you to get various response parameters, such as a status code, headers, HTTP version, and more.

Status code

To get the status code of a response, use the HttpResponse.status property:

import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* val httpResponse: HttpResponse = client.get("https://ktor.io/") if (httpResponse.status.value in 200..299) { println("Successful response!") }

Headers

The HttpResponse.headers property allows you to get a Headers map containing all response headers.

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.

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:

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:

val values = headers["X-Multi-Header"]!! // ["1, 2", "3"]

Receive response body

Raw body

To receive a raw body of a response, call the body function and pass the required type as a parameter. The code snippet below shows how to receive a raw body as a String:

val httpResponse: HttpResponse = client.get("https://ktor.io/") val stringBody: String = httpResponse.body()

Similarly, you can get a body as a ByteArray:

val httpResponse: HttpResponse = client.get("https://ktor.io/") val byteArrayBody: ByteArray = httpResponse.body()

A runnable example below shows how to get a response as a ByteArray and save it to a file:

val client = HttpClient() val file = File.createTempFile("files", "index") runBlocking { val httpResponse: HttpResponse = client.get("https://ktor.io/") { onDownload { bytesSentTotal, contentLength -> println("Received $bytesSentTotal bytes from $contentLength") } } val responseBody: ByteArray = httpResponse.body() file.writeBytes(responseBody) println("A file saved to ${file.path}") }

The onDownload() extension function in the example above is used to display download progress.

For non-streaming requests, the response body is automatically loaded and cached in memory, allowing repeated access. While this is efficient for small payloads, it may lead to high memory usage with large responses.

To handle large responses efficiently, use a streaming approach, which processes the response incrementally without saving it in memory.

JSON object

With the ContentNegotiation plugin installed, you can deserialize JSON data into a data class when receiving responses:

val customer: Customer = client.get("http://localhost:8080/customer/3").body()

To learn more, see Receive and send data.

Multipart form data

When receiving a response that contains multipart form data, you can read its body as a MultiPartData instance. This allows you to process form fields and files included in the response.

The example below demonstrates how to handle both text form fields and file uploads from a multipart response:

val response = client.post("https://myserver.com/multipart/receive") val multipart = response.body<MultiPartData>() multipart.forEachPart { part -> when (part) { is PartData.FormItem -> { println("Form item key: ${part.name}") val value = part.value // ... } is PartData.FileItem -> { println("file: ${part.name}") println(part.originalFileName) val fileContent: ByteReadChannel = part.provider() // ... } } part.dispose() }

Form fields

PartData.FormItem represents a form field, which values can be accessed through the value property:

when (part) { is PartData.FormItem -> { println("Form item key: ${part.name}") val value = part.value // ... } }

File uploads

PartData.FileItem represents a file item. You can handle file uploads as byte streams:

when (part) { is PartData.FileItem -> { println("file: ${part.name}") println(part.originalFileName) val fileContent: ByteReadChannel = part.provider() // ... } }

Resource cleanup

Once the form processing is complete, each part is disposed of using the .dispose() function to free resources.

part.dispose()

Streaming data

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 and I/O utilities.

Sequential chunk processing

To process the response sequentially in chunks, use HttpStatement with a scoped execute block.

The following example demonstrates reading a response in chunks and saving it to a file:

val client = HttpClient(CIO) val file = File.createTempFile("files", "index") val stream = file.outputStream().asSink() val fileSize = 100 * 1024 * 1024 val bufferSize: Long = 1024 * 1024 runBlocking { client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> val channel: ByteReadChannel = httpResponse.body() var count = 0L stream.use { while (!channel.exhausted()) { val chunk = channel.readRemaining(bufferSize) count += chunk.remaining chunk.transferTo(stream) println("Received $count bytes from ${httpResponse.contentLength()}") } } } println("A file saved to ${file.path}") }

Using ByteReadChannel.readRemaining() retrieves all available bytes in the channel, while Source.transferTo() directly writes the data to the file, reducing unnecessary allocations.

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

The ByteReadChannel.copyAndClose() function copies all remaining bytes from a ByteReadChannel to a ByteWriteChannel and then closes both channels automatically:

client.prepareGet("https://httpbin.org/bytes/$fileSize").execute { httpResponse -> val channel: ByteReadChannel = httpResponse.body() channel.copyAndClose(file.writeChannel()) 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

The ByteReadChannel.readTo() function writes bytes directly to a RawSink without intermediate buffers:

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.

23 January 2026