Ktor HttpClient зависает в runBlocking

Я проверяю рецепты в App Store на сервере Kotlin, используя Ktor HttpClient (версия Ktor 1.2.1). Вот мой код:

class AppStoreClient(
        val url: String,
        val password: String,
        val excludeOldTransactions: Boolean = true
) {
    private val objectMapper = ObjectMapperFactory.defaultObjectMapper()
    private val client = HttpClient(Apache /* tried with CIO as well */) {
        install(JsonFeature) {
            serializer = JacksonSerializer()
        }
    }

    suspend fun validate(receipt: String): VerifyReceiptResponse {
        val post = client.post<String> {
            url([email protected])
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
            body = VerifyReceiptRequest(
                    receipt,
                    password,
                    excludeOldTransactions
            )
        }

        // client.close()

        // Apple does not send Content-Type header ¯\_(ツ)_/¯
        // So Ktor's deserialization is not working here and
        // I have to manually deserialize the response.
        return objectMapper.readValue(post)
    }
}

А вот тестирую:

fun main() = runBlocking {
    val client = AppStoreClient("https://sandbox.itunes.apple.com/verifyReceipt", "<password>")

    println(client.validate("<recipe1>"))
    // println(client.validate("<recipe2>"))
    // println(client.validate("<recipe3>"))
}

Я получил все ответы (один или три) на выходе, но затем мое приложение просто зависло и никогда не выходит из метода main. Похоже, runBlocking все еще чего-то ждет, например client.close. Действительно, если я закрою клиент после первого запроса, приложение успешно завершится, но это заставит меня создавать клиент для каждого отдельного запроса проверки. Конфигурация конвейера клиента кажется трудоемкой, а AppStoreClient предназначен для долгоживущего объекта, поэтому я подумал, что клиент может поделиться своим жизненным циклом (возможно, даже с внедрением зависимости).

Является ли io.ktor.client.HttpClient долгоживущим объектом, который можно повторно использовать для нескольких запросов, или мне следует создавать новый для каждого запроса?

Если да, то что я делаю не так, поэтому зависает runBlocking?


P.S. Код работает с Ktor 1.1.1! Это ошибка?


П.П.С. Этот код также висит:

fun main() {
    val client = AppStoreClient("...", "...")

    runBlocking {
        println(client.validate("..."))
        println(client.validate("..."))
        println(client.validate("..."))
    }

    runBlocking {
        println(client.validate("..."))
        println(client.validate("..."))
        println(client.validate("..."))
    }
}

Так что я мог бы серьезно подумать о закрытии клиента.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
2 070
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Is the io.ktor.client.HttpClient a long-lived object that can be re-used for multiple requests or should I create a new one for each request?

Да, рекомендуется использовать один HttpClient, так как некоторые ресурсы (например, пул потоков в случае ApacheHttpClient) выделены под капотом, и нет причин каждый раз создавать новый клиент.

If yes, what am I doing wrong with it, so the runBlocking hangs?

Ваша проблема с закрытием клиента, а не с самими сопрограммами, рассмотрим этот пример, который также «зависает»:

fun main() {
    val client = HttpAsyncClients.createDefault().also {
        it.start()
    }
}

Итак, в моей практике, закрытие клиентской ответственности разработчика, примерно так:

fun main() {
    val client = HttpAsyncClients.createDefault().also {
        it.start()
    }

    client.close() // we're good now

}

Или используйте Runtime.addShutodownHook в более сложных приложениях.

P.S. The code works with Ktor 1.1.1! Is it a bug?

Я думаю, это реальный вопрос, что 1.1.1 делает, а 1.2.1 нет (или наоборот)


УПД.

Согласно Ktor Client документация, вы должны закрыть клиент вручную:

suspend fun sequentialRequests() {
    val client = HttpClient()

    // Get the content of an URL.
    val firstBytes = client.get<ByteArray>("https://127.0.0.1:8080/a")

    // Once the previous request is done, get the content of an URL.
    val secondBytes = client.get<ByteArray>("https://127.0.0.1:8080/b")

    client.close()
}

Другие вопросы по теме