In Ktor, how do you create an HttpClient and read the status and body of a GET response?
answer
- Two steps: send, then read
- Request functions suspend and return a response
- HttpResponse carries status, headers, payload
- bodyAsText() or reified body<T>()
- submitForm for x-www-form-urlencoded
basics
~10 sConstruct 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 sYou 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 linesval 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
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.
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.
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.
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