KtorMiddleTechnical

Что такое объект ApplicationCall в Ktor?

ApplicationCall — центральный объект одного HTTP-обмена в Ktor: через него читают запрос (call.request), отправляют ответ (call.respond) и передают данные между плагинами через call.attributes.

Что такое ApplicationCall

ApplicationCall — это интерфейс в Ktor, представляющий единственный HTTP-обмен «запрос — ответ». Каждый входящий запрос порождает новый экземпляр ApplicationCall; он живёт ровно столько, сколько обрабатывается соответствующий запрос, и уничтожается после отправки ответа.

Интерфейс объявлен в пакете io.ktor.server.application и содержит три ключевых свойства:

  • call.request: ApplicationRequest — входящий запрос: метод, URI, заголовки (headers["Authorization"]), query-параметры (queryParameters["page"]), тело (receiveText(), receive<T>()).
  • call.response: ApplicationResponse — исходящий ответ: установка заголовков, статуса. Обычно управляется через call.respond().
  • call.attributes: Attributes — типизированный key-value контейнер для передачи данных между плагинами и route-обработчиками без каста через Any.

Жизненный цикл

Ktor обрабатывает каждый запрос в coroutine. Конвейер плагинов (ApplicationCallPipeline) проходит через фазы Setup, Monitoring, Features, Call, Fallback. На каждой фазе плагин получает текущий ApplicationCall через PipelineContext<Unit, ApplicationCall>. После вызова call.respond() ответ считается «завершённым» — повторный вызов бросает ResponseAlreadySentException.

Передача данных через Attributes

Атрибуты — рекомендованный способ передавать контекст (например, аутентифицированного пользователя) из плагина в обработчик без глобального состояния:

// Определяем ключ один раз
val UserIdKey = AttributeKey<Long>("UserId")

// Плагин аутентификации устанавливает атрибут
val authPlugin = createApplicationPlugin("AuthPlugin") {
    onCall { call ->
        val token = call.request.headers["Authorization"] ?: run {
            call.respond(HttpStatusCode.Unauthorized)
            return@onCall
        }
        val userId = verifyToken(token)   // ваша логика
        call.attributes.put(UserIdKey, userId)
    }
}

// Route-обработчик читает атрибут
fun Application.module() {
    install(authPlugin)
    routing {
        get("/profile") {
            val userId = call.attributes[UserIdKey]
            call.respond(fetchProfile(userId))
        }
    }
}

Чтение тела и сериализация

@Serializable
data class CreateUserRequest(val name: String, val email: String)

fun Application.module() {
    install(ContentNegotiation) { json() }
    routing {
        post("/users") {
            // call.receive<T>() использует установленный ContentNegotiation
            val body = call.receive<CreateUserRequest>()
            val id = userService.create(body.name, body.email)
            call.respond(HttpStatusCode.Created, mapOf("id" to id))
        }
    }
}

Подводные камни

  • Двойной respond. Второй вызов call.respond() в одном обработчике бросает ResponseAlreadySentException. Нужно явно завершать ветку через return после respond().
  • Тело можно прочитать только один раз. receiveChannel() или receive<T>() потребляют поток байт; повторное чтение вернёт пустые данные или ошибку без явного кэширования (receiveText() + разбор вручную).
  • Thread safety атрибутов. call.attributes не thread-safe. Не обращайтесь к ним из параллельных coroutine-контекстов без внешней синхронизации.
  • Смешение ApplicationCall с тестами. В testApplication { } используется TestApplicationCall; некоторые расширения (например, WebSocket-специфичные) недоступны в тестовом движке.
  • Неправильный статус по умолчанию. Если вызвать call.respond(someObject) без явного HttpStatusCode, Ktor ответит 200. Для ошибочных кейсов всегда передавайте статус явно.
  • Blocking в coroutine. Если внутри обработчика вызвать blocking IO (JDBC без withContext(Dispatchers.IO)), это заблокирует event loop и резко снизит throughput.
  • Отсутствие ContentNegotiation. Вызов call.receive<T>() без установленного плагина ContentNegotiation бросает UnsupportedMediaTypeException в рантайме, а не на старте.
  • call.respond vs respondText vs respondBytes. Неверный выбор метода приводит к лишней сериализации или неправильному Content-Type в ответе.

Common mistakes

  • Путать термин «application call» с соседним механизмом Ktor.
  • Не называть границу lifecycle, transaction, thread или request для «application call».
  • Игнорировать production-эффекты «application call»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «application call» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «application call».
  • Уточнить, какие настройки или API меняют «application call» в реальном сервисе.

Sources

Related topics