Fix SocketTimeoutException on Retrofit with Coroutine

Bram Yeh
2 min readMay 23, 2020

In the previous article, I mention that our project uses suspend fun getString(): String? to implement all methods of Retrofit. To avoid uncaught exceptions, we also use try-catch to wrap all the suspend methods.

However, it didn’t work, we still get java.net.SocketTimeoutException

Caused by java.net.SocketTimeoutException
failed to connect to xxxxxxx/xxx.xxx.xxx.xxx (port xxx) from /xxx.xxx.xxx.xxx (port xxx) after 10000ms

Update

Jake Wharton wrote a great article which explains the inner architecture of Retrofit and suspend functions, Exceptions and proxies and coroutines, oh my!.

Coroutine Catch Handler Scenarios

Before sharing why we get the exception and app crash even after we adopt try-catch, we introduce the coroutine catch handler mechanism.

Whenever an exception happens during Coroutine, it’s redirected to the handleCoroutineException() to handle this exception.

public fun handleCoroutineException(
context: CoroutineContext,
exception: Throwable) {
// Invoke an exception handler from the context if present
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(
context,
handlerException(exception, t))
return
}
// If a handler is not present in the context
// or an exception was thrown,
// fallback to the global handler
handleCoroutineExceptionImpl(context, exception)
}

Invoke an exception handler from the CoroutineContext if present.

In JVM (which also Android Coroutine base on), even handleCoroutineExceptionImpl() looks for exception handlers using the ServiceLoader and forwards the exception to all the ones it can find. It also forwards the exception to the handler of the current thread.

internal actual fun handleCoroutineExceptionImpl(
context: CoroutineContext,
exception: Throwable) {
// use additional extension handlers
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
// Use thread's handler
// if custom handler failed to handle exception
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler
.uncaughtException
(
currentThread,
handlerException(exception, t))
}
}
// use thread's handler
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler
.uncaughtException
(currentThread, exception)
}

kotlinx-coroutines-android has exception-pre-handler which also call thread.uncaughtExceptionHandler.uncaughtException(thread, exception) differently.

Root Cause

Android throws SocketTimeoutException, pass through OKHTTP, and finally, in the coroutine framework, it would dispatch this SocketTimeoutException to thread’s hander.

Workaround

We didn’t find the solution, but I wiped out this issues by a workaround: before sending a network connect, our app will verify the network status, if the network isn’t available, stop the query.

With OKHttp’s Inteceptor, I wrote a custom interceptor, ConnectVerifierInterceptor, to verify network status first and return IOException if the network isn’t available.

fun isNetworkAvailable(): Boolean {
val connectivityManager = getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
return connectivityManager?.activeNetworkInfo?.isConnected == true
}
class ConnectVerifierInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response {
if (!isNetworkAvailable() {
throw IOException("No Network Available!")
}
val request = chain.request()
return chain.proceed(request)
}
}

And inject the ConnectVerifierInterceptor into OKHTTP client as application interceptor.

val okHttpClient = OkHttpClient().newBuilder()
.addInterceptor(ConnectVerifierInterceptor())
.build()

Then, I use this okHttpClient to build a Retrofit API interface.

val server = Retrofit.Builder().baseUrl(XXXX).client(okHttpClient).create(APIServer::class.java)

If there are any similar issues and you find a better solution, please share your prescription with me. Sincerely I don’t regard our fixing as a good solution, but it just reduces the possibility of SocketTimeoutException. Thanks very much.

--

--