Ktor HttpClient завис в рабочем режиме - PullRequest
2 голосов
/ 28 мая 2019

Я проверяю рецепты App Store в Kotlin на стороне сервера, используя Ktor's 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(this@AppStoreClient.url)
            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 зависает?


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


PPS Этот код также зависает:

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("..."))
    }
}

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

1 Ответ

1 голос
/ 28 мая 2019

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

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

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

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

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. Код работает с Ktor 1.1.1! Это ошибка?

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


UPD .

Согласно 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()
}
...