Skip to content

io.ktor

The io.ktor pack ships the ktor surface klio supports — the common modules (io.ktor.utils.io, io.ktor.util, io.ktor.http, io.ktor.events), the HTTP client, and an embedded server — built from the real upstream ktor sources plus klio-authored platform actuals. It is opt-in: installing the pack registers it, but nothing loads until a program enables a feature, because not every program needs the network.

Features

Features mirror ktor's Gradle module structure — one feature per module, all opt-in via --feature io.ktor/<name>. Nothing loads by default; a higher-level feature pulls the modules it needs through requires, so enabling the client also enables everything under it.

Feature Surface (io.ktor.…) Requires
io utils.io.* — ByteChannel, locks, charsets —
utils util.* — collections, pipeline, date, log io
http http.* — URLs, headers, status, content utils
events events.* — the event bus utils
client client.* — HttpClient + plugins http, events
server server.* — embeddedServer, routing http, events
client-serialization ContentNegotiation { json() } (client) client
server-serialization typed respond/receive + json() (server) server

So a bare channel program enables --feature io.ktor/io; a client program enables --feature io.ktor/client (which transitively pulls http, utils, io, and events). The *-serialization features additionally pull the kotlinx.serialization pack's json feature.

Client

import io.ktor.client.HttpClient
import io.ktor.http.HttpMethod
import kotlinx.coroutines.runBlocking

fun main() {
    val client = HttpClient()
    val resp = runBlocking {
        client.get("https://httpbin.org/get")
    }
    println("status=${resp.status}")
    println("body=${resp.bodyAsText()}")
    client.close()
}

Run it with klio run --feature io.ktor/client program.kt.

Engine

The host binding (src/ktor_client/ktor_client.zig) wires the shim into a small blocking HTTP/1.1 transport built on the platform sockets: blocking, single-thread, modest dependency footprint. A request returns a flat Array<String> shaped [status, body, contentType, k1, v1, k2, v2, …] that the shim rebuilds into HttpResponse. Returning primitives from native bindings avoids the cost (and bugs) of constructing Kotlin class instances directly from Zig.

Any module that calls HostBindings.register( "io.ktor.client.engine.__kktor_request", myFn) shadows the default engine. To wire it in, add the module to the CLI's mergedHostBindings() after ktor_client.hostBindings() — later registrations win (swap the transport, route through an in-memory mock during tests, intercept for tracing). The engine contract is the StdlibFn shape every host binding shares (*const fn (ctx: *CallCtx) Allocator.Error!EvalResult); arguments are [method, url, body, headers] and the return is the flat string array.

Server

embeddedServer(CIO, port) { … } runs a blocking HTTP/1.1 server on a single accept loop (the native __kktor_serve binding). The module lambda installs plugins and a routing { … } table; each handler runs against an ApplicationCall exposing the request and collecting the response.

import io.ktor.server.engine.embeddedServer
import io.ktor.server.cio.CIO
import io.ktor.server.routing.routing
import io.ktor.server.routing.route
import io.ktor.server.response.respondText
import io.ktor.server.response.respond
import io.ktor.server.request.receiveText
import io.ktor.server.request.receive
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.contentnegotiation.install
import io.ktor.serialization.kotlinx.json.json
import io.ktor.http.HttpStatusCode
import io.ktor.http.ContentType
import kotlinx.serialization.Serializable

@Serializable
data class User(val id: Int, val name: String)

fun main() {
    embeddedServer(CIO, port = 8080) {
        install(ContentNegotiation) { json() }
        routing {
            get("/users/{id}") {
                val id = call.parameters["id"]
                val q = call.request.queryParameters["q"]
                val tag = call.request.headers["X-Tag"]
                call.respondText("id=$id q=$q tag=$tag", status = HttpStatusCode.OK)
            }
            post("/items") {
                val body = call.receiveText()
                call.response.headers.append("X-Made", "yes")
                call.respondText("created:$body", contentType = ContentType.Text.Plain, status = HttpStatusCode.Created)
            }
            post("/users") {
                val u = call.receive<User>()                 // typed JSON in
                call.respond(HttpStatusCode.Created, User(u.id, u.name + "!"))  // typed JSON out
            }
            route("/api/v1") {                                // nested prefix
                get("/ping") { call.respondText("pong") }     // -> /api/v1/ping
            }
            get("/files/{path...}") {                         // tailcard
                call.respondText("file=${call.parameters["path"]}")
            }
        }
    }.start(wait = true)
}

Run it with klio run --feature io.ktor/server-serialization server.kt (use io.ktor/server if you do not need typed JSON), then drive it:

curl -i 'http://127.0.0.1:8080/users/42?q=hi' -H 'X-Tag: abc'
curl -i -X POST --data 'widget' http://127.0.0.1:8080/items
curl -i -X POST --data '{"id":7,"name":"Ada"}' http://127.0.0.1:8080/users

The handler surface: call.parameters (path {name} captures), call.request.queryParameters, call.request.headers (case-insensitive), call.receiveText() / call.receive<T>(), call.respondText(text, contentType, status), call.respond(status, value) (typed JSON), call.response.headers.append(name, value), and call.response.status(code).

Route patterns support {name} (one captured segment), {name...} (a tailcard capturing the rest of the path), and * (any one segment, not captured); route(prefix) { … } nests, prepending its prefix to the routes inside it. The first registered route whose method and pattern match wins.

start(wait = true) blocks the calling thread; start(wait = false) dispatches the accept loop onto the coroutine worker pool and returns, so a program can start the server, do other work, and exit (the daemon serve loop is abandoned cleanly at the run boundary).

Install

./zig-out/bin/klio pack build kotlin-klio/klio-ktor
./zig-out/bin/klio pack install target/packs/io.ktor.klio-pack

What is not included

  • Streaming / SSE bodies.
  • WebSocket support.
  • Regex route segments and per-route plugins / interceptors.
  • Pluggable client engines beyond the built-in transport. The slot is there if you want to swap in another one — wire a new module into mergedHostBindings() and adjust the binding manifest.