Я проверяю рецепты в 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("..."))
}
}
Так что я мог бы серьезно подумать о закрытии клиента.
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()
}