skip to content

Ktor Client

The Ktor client shares the plugin model with the server, with a request DSL, content negotiation, default request configuration and WebSocket support. Interviewers like that the same library covers both sides, particularly in multiplatform projects.

on this pageshow

questions

6

In Ktor, how do you create an HttpClient and read the status and body of a GET response?

level: juniorimportance: must knowfreq 78%

answer

  1. Two steps: send, then read
  2. Request functions suspend and return a response
  3. HttpResponse carries status, headers, payload
  4. bodyAsText() or reified body<T>()
  5. submitForm for x-www-form-urlencoded

basics

~10 s

Construct an HttpClient with an engine, then call the suspend function client.get(url). It returns an HttpResponse: read the code from response.status and the payload from response.bodyAsText() or the typed response.body<T>().

solid answer

~40 s

You build a client once — `val client = HttpClient(CIO)` — where `CIO` is the engine artifact you depend on. Request functions (`get`, `post`, `put`, `delete`, or the generic `request`) are **suspend** functions and, since Ktor 2.0, they return an `HttpResponse` rather than the parsed body. Each takes an optional builder block where you add `parameter("page", 2)`, `header(...)`, `accept(ContentType.Application.Json)`, or `setBody(...)`. From the response you read `response.status` (an `HttpStatusCode`), `response.headers`, and then the payload: `response.bodyAsText()` for raw text or `response.body<User>()` for a typed object once a converter is configured. Form posts have a dedicated helper, `submitForm`, which encodes a `Parameters` set as a form body. Nothing is thrown for a 404 by default, so checking `status` is part of reading the response.

code

kotlin · 13 lines
kotlin
val client = HttpClient(CIO)

suspend fun fetchPage(): String {
    val response: HttpResponse = client.get("https://api.example.com/users") {
        parameter("page", 2)
        header("X-Trace-Id", "abc-123")
        accept(ContentType.Application.Json)
    }
    if (response.status != HttpStatusCode.OK) {
        error("unexpected status ${response.status}")
    }
    return response.bodyAsText()
}

go deeper

for a junior

Recall the two-step shape: a suspend request function returns an HttpResponse, and the payload is a separate call — bodyAsText() or body<T>(). Be able to write a GET with a query parameter from memory.

for a middle

Be ready to explain the request builder in detail — parameter, header, contentType, setBody, accept — and why the status must be checked explicitly rather than assumed successful.

for a senior

Show the production instincts: stream large payloads with prepareGet/execute instead of buffering them, install a timeout plugin, and never let an unchecked status silently become a deserialization error.

for a principal

Own the shape of the integration layer: call sites should use typed functions over a shared, configured client rather than hand-building URLs and headers, so cross-cutting concerns live in one place.

## What the Ktor client is The Ktor client is a Kotlin HTTP client whose API is a DSL rather than a builder-object hierarchy. One type, `HttpClient`, is the entry point; everything else — engines, plugins, request builders — hangs off it. The same plugin model that configures a Ktor server also configures the client, which is why the two sides of the framework feel identical. ## Creating a client `HttpClient` is constructed with an *engine factory*: `HttpClient(CIO)`, `HttpClient(OkHttp)`, `HttpClient(Java)`, `HttpClient(Darwin)`. The factory comes from an engine artifact you add as a dependency; the core artifact `ktor-client-core` contains no engine at all. A no-argument `HttpClient()` also exists and resolves an engine from whichever engine artifacts are on the classpath, failing at runtime if none is present. The optional configuration block installs plugins and sets client-wide options: HttpClient(CIO) { install(ContentNegotiation) { json() }; install(HttpTimeout) { requestTimeoutMillis = 5000 } } A client is a real resource — it owns the engine's threads and connections — so you create it once and reuse it rather than per call. ## Making a request Request functions are extension functions on `HttpClient` and are **suspend**, so they must be called from a coroutine. Since Ktor 2.0 they return `HttpResponse`; in Ktor 1.x they were generic (`get<User>()`) and returned the deserialized body directly, which is why old snippets look different. The available shapes are the verb helpers (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`) and the generic `request(url) { method = HttpMethod.Get }`. ## The request builder The trailing lambda is an `HttpRequestBuilder`. Useful calls inside it: - `parameter("page", 2)` — appends a URL query parameter. - `header("X-Trace-Id", id)` or a `headers { append(...) }` block. - `accept(ContentType.Application.Json)` — sets the `Accept` header. - `contentType(ContentType.Application.Json)` — sets the request's `Content-Type`. - `setBody(value)` — the request payload: a `String`, a byte array, a channel, or an object when a converter is configured. - `url { ... }` — full control over scheme, host, port, path segments and `parameters`. For HTML-style form posts there is `submitForm(url, formParameters = parameters { append("user", "ann") })`, which encodes the parameters as an `application/x-www-form-urlencoded` body. Passing `encodeInQuery = true` instead sends them in the URL's query string. ## Reading the response `HttpResponse` exposes the metadata directly: - `response.status` — an `HttpStatusCode`; compare with `HttpStatusCode.OK`, or use `status.value` / `status.isSuccess()`. - `response.headers` — the response headers. - `response.bodyAsText()` — suspend, decodes the payload as text. - `response.body<User>()` — suspend and reified; converts the payload into a typed object. This requires a converter (the `ContentNegotiation` client plugin); without one it throws `NoTransformationFoundException`. - `response.bodyAsChannel()` / `response.bodyAsBytes()` — raw access. Nothing about a non-2xx status stops any of this by default: a 404 is simply a response with `status == HttpStatusCode.NotFound` and, usually, an error payload. Checking the status before deserializing is therefore part of correct client code. ## Streaming large responses The verb helpers read the whole payload. When the body is large or you want to consume it incrementally, use the *prepared* form: `client.prepareGet(url).execute { response -> ... }`. The response body is a live stream that is only valid inside the `execute` block, and the connection is released when the block returns. Trying to keep the response object and read it afterwards is the classic misuse. ## Common mistakes Treating `client.get(...)` as if it returned the parsed model (Ktor 1.x muscle memory); forgetting that an engine artifact must be on the classpath; calling `body<T>()` with no converter installed; and assuming an error status raises an exception. Each of these produces a confusing failure that has nothing to do with the server being talked to.

  • How do you add query parameters and headers to a single Ktor client request?
    Inside the request's trailing lambda: `parameter("page", 2)` appends a query parameter, `header("X-Trace-Id", id)` adds one header, and a `headers { append(...) }` block adds several. For full control over the URL there is `url { path(...); parameters.append(...) }`. Everything in the block applies to that call only.
  • What does submitForm do that a plain post does not?
    `submitForm` takes a `Parameters` set and encodes it for you as an `application/x-www-form-urlencoded` body, setting the content type accordingly — you do not hand-build the encoded string. Passing `encodeInQuery = true` puts the same parameters in the URL query instead of the body.
  • How do you consume a very large response without holding it in memory?
    Use the prepared form: `client.prepareGet(url).execute { response -> ... }`. Inside the block the body is available as a stream (for example via `bodyAsChannel()`), and you process it incrementally. The stream is only valid inside `execute`; the response object is not usable after the block returns.

Think of the response object as an unopened envelope: the postmark (status and headers) is readable immediately, but you still have to open it to get the letter.

saying these in an interview costs you the question

  • Says client.get() returns the deserialized object directly
  • Expects a 404 to throw automatically
  • Thinks request functions are blocking, not suspend
  • Cannot name any engine artifact the client needs
  • Calls bodyAsText() on a streamed response after the block

context

open as a page

Why does a Ktor client call to body<User>() fail without ContentNegotiation installed?

level: middleimportance: must knowfreq 58%

basics

~20 s

Ktor's client core only converts payloads to bytes, text and channels. Typed conversion needs a converter, supplied by the client ContentNegotiation plugin; without it body<User>() throws NoTransformationFoundException because no registered converter can produce that type.

open as a page

Why should a Ktor HttpClient be created once and reused instead of per request?

level: seniorimportance: must knowfreq 55%

basics

~20 s

A Ktor HttpClient owns an engine with threads, connections and plugin state such as cookie storage and cached tokens. Creating one per request multiplies those resources, discards warm connections and state, and leaks unless every instance is closed.

open as a page

What is a Ktor client engine, and how do you choose one for JVM, Android, or iOS?

level: middleimportance: should knowfreq 52%

basics

~20 s

A Ktor client engine is the pluggable implementation that actually performs requests. HttpClient is the shared API; the engine comes from a separate artifact passed as HttpClient(CIO), HttpClient(OkHttp), HttpClient(Darwin), and differs per platform and capability.

open as a page

In the Ktor client, what does expectSuccess change, and how do you handle a 404?

level: middleimportance: should knowfreq 46%

basics

~10 s

Ktor's client sets expectSuccess to false by default, so a 404 returns a normal HttpResponse you must inspect via response.status. Setting expectSuccess = true makes non-2xx responses throw ClientRequestException, ServerResponseException or RedirectResponseException instead.

open as a page

In the Ktor client, how do you open a WebSocket connection and exchange frames?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

Install the WebSockets plugin on the HttpClient, then call client.webSocket("wss://host/path") { ... }. Inside the block you send with send(Frame.Text(...)) and read from incoming; the session closes when the block returns.

open as a page