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がプライベートかパブリックルーティング可能かをすばやく確認できます — 完全にブラウザ上で動作し、何もアップロードされません。通常のツールが手元にないフィールドでのデバッグに便利です。
参考資料
- OkHttpドキュメント — タイムアウトとインターセプターの公式ガイド
- Retrofitドキュメント — クライアントの設定
- Android: ネットワーク状態の読み取り — ConnectivityManager API

