Setiap permintaan HTTP di Ktor diwakili oleh satu objek ApplicationCall yang merangkum request dan response. Handler route menerima ApplicationCall dan memanipulasinya untuk membaca input, menulis output, serta mengatur status dan header. Memahami API ApplicationCall adalah prasyarat sebelum membahas content negotiation, validation, dan error handling. Pada chapter ini kita akan membedah call.respondText, call.receive, status code, dan header melalui model yang dapat dijalankan langsung.

ApplicationCall #

ApplicationCall adalah jembatan antara handler Kotlin dan request/response HTTP. Objek ini memiliki dua sisi: call.request untuk membaca input dan call.response untuk menulis output. Ktor juga menyediakan helper singkat seperti call.receiveText() untuk membaca body mentah dan call.respondText() untuk menulis body dengan content type eksplisit.

Handler Ktor biasanya berbentuk suspend fun ApplicationCall.handleFoo() — extension function pada ApplicationCall. Karena suspend, handler dapat memanggil operasi asinkron seperti query database tanpa memblok thread.

API Inti ApplicationCall #

APIPeran
call.receiveText()Membaca body mentah sebagai String
call.receive<T>()Membaca body sebagai objek T (perlu ContentNegotiation)
call.respondText(body, contentType, status)Menulis body teks dengan content type dan status
call.respond(value)Menulis objek (perlu ContentNegotiation)
call.response.status(HttpStatusCode)Mengatur status code
call.request.headers["Authorization"]Membaca header request

Membaca Body Request #

Untuk endpoint POST atau PUT, body request biasanya berisi data yang akan disimpan. call.receiveText() mengembalikan seluruh body sebagai String — cocok untuk payload kecil atau logging. Untuk payload terstruktur, call.receive<T>() mengonversi body ke objek Kotlin lewat plugin ContentNegotiation yang akan dibahas pada chapter berikutnya.

Validasi body wajib dilakukan oleh handler. Ktor tidak menolak body kosong secara otomatis — handler harus memeriksa body.isBlank() dan mengembalikan status 400 bila invalid.

call.respondText() adalah API paling dasar untuk menulis respons. Fungsi ini menerima body, content type opsional, dan status code opsional (default 200). Untuk respons terstruktur seperti JSON, handler memakai call.respond(objek) yang memicu ContentNegotiation menserialisasi objek ke JSON otomatis.

Status Code Umum #

StatusArtiKapan dipakai
200 OKPermintaan berhasilGET berhasil
201 CreatedResource baru dibuatPOST berhasil
400 Bad RequestInput invalidBody kosong, format salah
404 Not FoundResource tidak adaID tidak ditemukan
500 Internal ErrorKesalahan serverException tidak tertangani

Demo Request dan Response #

Agar API ApplicationCall dapat dijalankan tanpa Ktor nyata, kita akan memodelkannya sebagai class biasa dengan field requestHeaders, requestBody, responseStatus, responseBody, dan responseHeaders. Handler ditulis sebagai function biasa yang menerima ApplicationCall.

kotlin
// ktor_request_dan_response.kt
// Mendemonstrasikan konsep ApplicationCall, call.respondText, call.receive,
// status code, dan headers pada Ktor.

// Mock ApplicationCall dengan header dan body
class ApplicationCall(
    val method: String,
    val path: String,
    val requestHeaders: Map<String, String> = emptyMap(),
    val requestBody: String = ""
) {
    var responseStatus: Int = 200
    var responseHeaders: MutableMap<String, String> = mutableMapOf()
    var responseBody: String = ""

    fun receiveText(): String = requestBody

    inline fun <reified T> receive(): T {
        // Dalam Ktor nyata, ContentNegotiation plugin mengonversi JSON ke objek.
        // Di sini kita mock untuk String dan Int.
        return when (T::class) {
            String::class -> requestBody as T
            else -> throw IllegalArgumentException("Tipe ${T::class} tidak didukung")
        }
    }

    fun respondText(
        body: String,
        contentType: String = "text/plain",
        status: Int = 200
    ) {
        responseBody = body
        responseHeaders["Content-Type"] = contentType
        responseStatus = status
    }

    fun respond(body: String, status: Int = 200) {
        responseBody = body
        responseStatus = status
    }
}

// Handler Ktor biasanya: suspend fun ApplicationCall.handleFoo()
fun handleRoot(call: ApplicationCall) {
    call.respondText("Halo dari Ktor", contentType = "text/plain")
}

fun handleUserById(call: ApplicationCall, id: Int) {
    if (id <= 0) {
        call.respondText("Bad Request: id tidak valid", status = 400)
        return
    }
    call.respondText("""{"id":$id,"nama":"Budi"}""", contentType = "application/json")
}

fun handleCreateUser(call: ApplicationCall) {
    val body = call.receiveText()
    if (body.isBlank()) {
        call.respondText("Body tidak boleh kosong", status = 400)
        return
    }
    call.respondText("Created: $body", contentType = "application/json", status = 201)
}

fun main() {
    println("=== GET / ===")
    val c1 = ApplicationCall("GET", "/")
    handleRoot(c1)
    println("Status: ${c1.responseStatus}, Body: ${c1.responseBody}, Headers: ${c1.responseHeaders}")

    println("\n=== GET /users/42 ===")
    val c2 = ApplicationCall("GET", "/users/42")
    handleUserById(c2, 42)
    println("Status: ${c2.responseStatus}, Body: ${c2.responseBody}")

    println("\n=== GET /users/0 (invalid) ===")
    val c3 = ApplicationCall("GET", "/users/0")
    handleUserById(c3, 0)
    println("Status: ${c3.responseStatus}, Body: ${c3.responseBody}")

    println("\n=== POST /users (valid) ===")
    val c4 = ApplicationCall("POST", "/users", requestBody = """{"nama":"Siti"}""")
    handleCreateUser(c4)
    println("Status: ${c4.responseStatus}, Body: ${c4.responseBody}")

    println("\n=== POST /users (kosong) ===")
    val c5 = ApplicationCall("POST", "/users", requestBody = "")
    handleCreateUser(c5)
    println("Status: ${c5.responseStatus}, Body: ${c5.responseBody}")
}

Output yang Dihasilkan:

text
=== GET / ===
Status: 200, Body: Halo dari Ktor, Headers: {Content-Type=text/plain}

=== GET /users/42 ===
Status: 200, Body: {"id":42,"nama":"Budi"}

=== GET /users/0 (invalid) ===
Status: 400, Body: Bad Request: id tidak valid

=== POST /users (valid) ===
Status: 201, Body: Created: {"nama":"Siti"}

=== POST /users (kosong) ===
Status: 400, Body: Body tidak boleh kosong

Mari kita bedah bagian penting kode di atas:

  1. class ApplicationCall(val method, val path, val requestHeaders, val requestBody): model dengan field request dan response terpisah. Field val untuk input (tidak berubah), var untuk output (diisi handler).
  2. fun respondText(body, contentType, status): menulis body, mengatur header Content-Type, dan status code sekaligus. API ini meniru call.respondText Ktor nyata.
  3. handleUserById(call, id): handler memvalidasi id terlebih dahulu. Bila id <= 0, kembalikan status 400 dan hentikan dengan return awal — pola fail-fast yang lazim di handler.
  4. handleCreateUser(call): membaca body via receiveText(), memeriksa isBlank(), dan mengembalikan 400 bila kosong. Bila valid, kembalikan 201 Created dengan body konfirmasi.
  5. val c4 = ApplicationCall("POST", "/users", requestBody = ...): simulasi permintaan POST dengan body. Named parameter meningkatkan keterbacaan konstruktor dengan banyak argumen.

Referensi Lanjutan #

  • Ktor requests — panduan resmi membaca parameter query, header, dan body request di Ktor
  • Ktor responses — panduan resmi mengembalikan respond(), status code, dan header di Ktor

Selanjutnya #

Setelah memahami baca-tulis dasar, langkah berikutnya adalah otomatisasi konversi JSON. Chapter Ktor Content Negotiation akan membahas plugin ContentNegotiation yang mengubah objek Kotlin menjadi JSON dan sebaliknya tanpa kode serialisasi manual.