AndroidでAPIコール時のSocketTimeoutExceptionを修正する(OkHttp / Retrofit)

intermediate📱 Android2026-06-03| Android 8.0以上 / KotlinまたはJava / OkHttp 4.x / Retrofit 2.x

Error Message

java.net.SocketTimeoutException: failed to connect to api.example.com/192.168.1.1 (port 443) from /192.168.1.2 (port 52341) after 15000ms
#ネットワーク#http#タイムアウト#ソケット#okhttp#retrofit

TL;DR

サーバーが応答する前にTCP接続がタイムアウトしました。ほとんどのケースで以下の3つの対処法が有効です:モバイルネットワークに適した現実的な値にOkHttpのタイムアウトを引き上げる、デバイスのネットワークからエンドポイントに実際に到達できるか確認する、一時的な障害に対するリトライロジックを追加する。

エラー内容

java.net.SocketTimeoutException: failed to connect to api.example.com/192.168.1.1 (port 443) from /192.168.1.2 (port 52341) after 15000ms

AndroidがAPIサーバーへのTCP接続を試みたものの、タイムアウト時間内(この場合は15秒)に応答が得られなかった場合に、logcatへ表示されます。接続は完了しませんでした。これは接続タイムアウトであり、読み取りタイムアウトではありません。デバッグを始める際、この違いは重要です。

原因

このエラーはいくつかの要因で発生します。明らかなものもあれば、そうでないものもあります:

  • サーバーに到達できない、またはファイアウォールがポート443をブロックしている — SYNパケットは送信されるが、サーバーがSYN-ACKを返さない
  • DNSが誤ったIPまたは古いIPを解決している — エラーメッセージ内のIPアドレスが想定するものと一致しない
  • デバイスが制限されたネットワーク上にある — 企業WiFi、VPN、またはアウトバウンド接続をブロックするホットスポット
  • タイムアウトの設定値が低すぎる — OkHttpのデフォルトは接続/読み取り/書き込みがそれぞれ10秒で、低速な3Gや混雑したネットワークでは頻繁に失敗する
  • サーバーが高負荷状態にある — 接続を受け付けているがキューが満杯でパケットがドロップされている

修正1:OkHttp / Retrofitのタイムアウトを調整する

ほとんどのRetrofitのセットアップはOkHttpを内部で使用しています。デフォルト値(接続10秒、読み取り10秒、書き込み10秒)は高速な接続では問題ありませんが、モバイルでは頻繁に失敗します。値を引き上げましょう:

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)   // TCP接続を確立するまでの時間
    .readTimeout(30, TimeUnit.SECONDS)      // サーバーの応答を待つ時間
    .writeTimeout(30, TimeUnit.SECONDS)     // リクエストボディを送信する時間
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

30秒はほとんどのモバイルAPIにとって妥当な出発点です。60秒を超えないようにしましょう — その時点では、明らかに戻ってこない接続をユーザーにスピナーで見つめさせているだけです。

修正2:一時的な障害に対するリトライロジックを追加する

OkHttpには組み込みのリトライ機能がありますが、控えめな設定になっています。一時的なネットワークの乱れやDNSの問題に対しては、バックオフ付きのカスタムリトライインターセプターの方が信頼性が高くなります:

class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        var lastException: IOException? = null

        while (attempt < maxRetries) {
            try {
                return chain.proceed(chain.request())
            } catch (e: SocketTimeoutException) {
                lastException = e
                attempt++
                if (attempt < maxRetries) {
                    Thread.sleep(1000L * attempt) // バックオフ: 1秒、2秒、3秒
                }
            }
        }
        throw lastException ?: IOException("Unknown error after $maxRetries retries")
    }
}

// OkHttpに適用する
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor(maxRetries = 3))
    .connectTimeout(30, TimeUnit.SECONDS)
    .build()

修正3:接続チェックで素早く失敗させる

タイムアウトまで30秒待つのはUXとして最悪です。呼び出しの前にネットワークの可用性を確認し、有用なメッセージを返してすぐに失敗させましょう:

fun isNetworkAvailable(context: Context): Boolean {
    val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val network = cm.activeNetwork ?: return false
    val capabilities = cm.getNetworkCapabilities(network) ?: return false
    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}

// Repositoryの中で
suspend fun fetchData(): Result<MyData> {
    if (!isNetworkAvailable(context)) {
        return Result.failure(IOException("No internet connection"))
    }
    return try {
        Result.success(api.getData())
    } catch (e: SocketTimeoutException) {
        Result.failure(e)
    }
}

修正4:DNSの解決をデバッグする

エラーに含まれるIPアドレスが最初の手がかりです。api.example.com/192.168.1.1 — これはプライベートIPです。パブリックAPIを呼び出しているのに192.168.x.xや10.x.x.xのアドレスが表示されている場合、DNSが誤ったホストを解決しています。古いキャッシュ、スプリットホライズンDNS、または設定の誤ったVPNが通常の原因です。

# デバイスが解決するIPを確認する
adb shell nslookup api.example.com

# 基本的な接続を確認する
adb shell ping -c 3 api.example.com

# ポート443に到達できるか確認する
adb shell nc -zv api.example.com 443

IPアドレスがおかしい場合は、WiFiからモバイルデータに切り替えて再試行してください。モバイルデータで成功した場合、WiFiネットワークがトラフィックをフィルタリングまたは誤ルーティングしています — コードの問題ではありません。

修正5:ネットワークセキュリティ設定を確認する

Android 9以降は、デフォルトでクリアテキストHTTPをブロックします。アプリが誤ってHTTPエンドポイント(URLのベースが間違っている、またはリダイレクトでHTTPに戻るケース)にアクセスした場合、AndroidはAが接続を切断してネットワークエラーとして表示します。設定を確認しましょう:

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
    </domain-config>
</network-security-config>

確認手順

修正を適用したら、以下の方法で実際に機能しているか確認しましょう:

  • エミュレーターではなく実機でテストする — エミュレーターのネットワーク動作は異なる
  • logcatをOkHttpタグでフィルタリングする — 例外のトレースではなく、リクエスト/レスポンスのログが表示されるはず
  • WiFiとモバイルデータの両方でテストして、ネットワーク固有の問題を捕捉する
  • デバッグビルドにOkHttpのロギングインターセプターを追加してタイミングの詳細を確認する:
// デバッグビルドのみ
if (BuildConfig.DEBUG) {
    val logging = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.HEADERS
    }
    builder.addInterceptor(logging)
}

Tips

エラーに表示されたIPがプライベートアドレス(10.x.x.x、172.16〜31.x.x、192.168.x.x)に見えるのにパブリックAPIにアクセスしている場合、DNSが誤った場所に誘導しています。ToolCraftのIP Subnet Calculatorを使えば、IPがプライベートかパブリックルーティング可能かをすばやく確認できます — 完全にブラウザ上で動作し、何もアップロードされません。通常のツールが手元にないフィールドでのデバッグに便利です。

参考資料

Related Error Notes