はじめに
記事を見てくださりありがとうございます!
@HayatoHanaoka と申します。
今日は仕事で扱っている技術でハマったので、その内容を記事にします。
誰か同じ問題で躓いている人の助けになれば幸いです!
また、「ここが違う」などのご感想があれば、お手柔らかに教えてください!🙏
何が起きたか?
Ktorの公式ドキュメントを読みながら実装したHttpClientで、2回目以降のリクエストを送れなかった。
当時のHttpCleintの実装内容は以下です。
import io.ktor.client.HttpClient
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
object HttpClient {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
engine {
requestTimeout = 10_000
}
}
suspend inline fun <reified T> get(
url: String,
headers: Map<String, String> = emptyMap()
): T {
client.use {
val res = it.get(url) {
headers.forEach {
(key, value) -> header(key, value)
}
}
return res.body()
}
}
上記のHttpClientを使ったAPIサーバーを作成し、8080ポートで起動。
そこからgetメソッドを使ったリクエスト送ると、1回目は無事200で成功。
curl -v localhost:8080/
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /v1/industries/UBI100100100/companies HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Length: 904
< Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"name": "Hoge"}
しかし、2回目からは500エラーになってしまいました。
curl -v localhost:8080/
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /v1/industries/UBI100100100/companies HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Content-Length: 0
<
* Connection #0 to host localhost left intact
原因
作成したHttpClientで.use{ }
を使っていたから
.use{ }
を使ってしまうと、コードブロック終了後に自動的にコネクションがclose()
され、再利用が不可能になります。
そのため、今回のように複数回のリクエスト別々に送ろうとすると500 Internal Server Error
となってしまってました。
解決法
2つあるのでどちらも紹介します。
-
client.use { }
を使わずコネクションを確立し、維持させる - リクエストのたびにHttpClientを生成し、
.use{ }
を使う
現時点での私の理解ですが...
前者は1度繋いだコネクションを再利用できるためパフォーマンスが良くなる反面、コネクションの悪用や誤作動等といったリスクがあります。
後者はコネクションを維持しないため安全性が高くコントロールしやすい反面、インスタンスの増加によるメモリ逼迫等のリスクがあります。
私は後者を取る方の利点が多いと思い、後者を選びました。
client.use { }
を使わず、コネクションを維持させる
import io.ktor.client.HttpClient
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
object HttpClient {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
engine {
requestTimeout = 10_000
}
}
suspend inline fun <reified T> get(
url: String,
headers: Map<String, String> = emptyMap()
): T {
val res = client.get(url) {
headers {
headers.map { item ->
append(item.key, item.value)
}
}
}
return res.body()
}
}
リクエストのたびにHttpClientを生成し、.use{ }
を使う
import io.ktor.client.HttpClient
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
object HttpClient {
fun createHttpClient(): HttpClient {
return HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
engine {
requestTimeout = 10_000
}
}
}
suspend inline fun <reified T> get(
url: String,
headers: Map<String, String> = emptyMap()
): T {
createHttpClient().use {
val res = it.get(url) {
headers {
headers.map { item ->
append(item.key, item.value)
}
}
}
return res.body()
}
}
}
補足
.use{}
を使うか、.close()
を使うかは、時と場合によって使い分けると良いかなと思いました。
特徴 | client.use | 明示的なclient.close() |
---|---|---|
リソース管理 | スコープ終了時に自動解放 | 手動で管理 |
クライアント再利用 | 再利用不可 | 再利用可能 |
エラー発生の可能性 | スコープ外利用でエラー | 再利用時にエラーは起きない |
適切な使いどころ | シングルリクエストや短期間の処理 | 複数回リクエストが必要な場合 |
最後に
ここは2日間くらい職場の先輩とハマってようやく納得のいく形にできました...。
世の中まだまだ知らないことだらけなので、もっと勉強しないとなぁ