0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【解決版】ktor で httpclient を作ったら、2回目以降のリクエストがサーバーエラーになる

Last updated at Posted at 2024-12-02

はじめに

記事を見てくださりありがとうございます!
@HayatoHanaoka と申します。
今日は仕事で扱っている技術でハマったので、その内容を記事にします。
誰か同じ問題で躓いている人の助けになれば幸いです!
また、「ここが違う」などのご感想があれば、お手柔らかに教えてください!🙏

何が起きたか?

Ktorの公式ドキュメントを読みながら実装したHttpClientで、2回目以降のリクエストを送れなかった。

当時のHttpCleintの実装内容は以下です。

HttpClient.kt
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つあるのでどちらも紹介します。

  1. client.use { } を使わずコネクションを確立し、維持させる
  2. リクエストのたびにHttpClientを生成し、.use{ } を使う

現時点での私の理解ですが...

前者は1度繋いだコネクションを再利用できるためパフォーマンスが良くなる反面、コネクションの悪用や誤作動等といったリスクがあります。

後者はコネクションを維持しないため安全性が高くコントロールしやすい反面、インスタンスの増加によるメモリ逼迫等のリスクがあります。

私は後者を取る方の利点が多いと思い、後者を選びました。

client.use { } を使わず、コネクションを維持させる

HttpClient.kt
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{ } を使う

HttpClient.kt
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日間くらい職場の先輩とハマってようやく納得のいく形にできました...。
世の中まだまだ知らないことだらけなので、もっと勉強しないとなぁ

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?