Ktor HttpClient 在 runBlocking 中挂起
Ktor HttpClient is hang in runBlocking
我正在使用 Ktor 的 HttpClient
(Ktor 版本为 1.2.1)在服务器端 Kotlin 中验证 App Store 配方。到目前为止,这是我的代码:
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
挂起?
P.S。该代码适用于 Ktor 1.1.1!这是一个错误吗?
P.P.S。此代码也挂起:
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,因为一些资源(如 thread-pool 在 ApacheHttpClient 的情况下)是在幕后分配的,没有理由每次都创建新的客户端。
If yes, what am I doing wrong with it, so the runBlocking hangs?
你的问题是关闭客户端,而不是协程本身,考虑这个例子,它也是 "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 不做什么(或 visa-versa)
UPD.
根据 Ktor Client documentation,您应该手动关闭客户端:
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()
}
我正在使用 Ktor 的 HttpClient
(Ktor 版本为 1.2.1)在服务器端 Kotlin 中验证 App Store 配方。到目前为止,这是我的代码:
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
挂起?
P.S。该代码适用于 Ktor 1.1.1!这是一个错误吗?
P.P.S。此代码也挂起:
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,因为一些资源(如 thread-pool 在 ApacheHttpClient 的情况下)是在幕后分配的,没有理由每次都创建新的客户端。
If yes, what am I doing wrong with it, so the runBlocking hangs?
你的问题是关闭客户端,而不是协程本身,考虑这个例子,它也是 "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 不做什么(或 visa-versa)
UPD.
根据 Ktor Client documentation,您应该手动关闭客户端:
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()
}