Retrofit 2: Catch connection timeout exception
AndroidRetrofitConnection TimeoutOkhttpAndroid Problem Overview
I have the following setup:
final OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setReadTimeout(5, TimeUnit.SECONDS);
okHttpClient.setConnectTimeout(5, TimeUnit.SECONDS);
RestAdapter.Builder builder = new RestAdapter.Builder()
.setEndpoint(ROOT)
.setClient(new OkClient(okHttpClient))
.setLogLevel(RestAdapter.LogLevel.FULL);
I am trying to handle the situation in which my server is down and the user gets a connection timeout exception, this is my logging:
java.net.SocketTimeoutException: failed to connect to /192.168.0.53 (port 3000) after 5000ms
Full logging: http://pastebin.com/gscCGb7x
Is there a way to route this into the retrofit failure method so I can handle it over there?
Thanks in advance!
Android Solutions
Solution 1 - Android
For Retrofit 2
Define a listener in your web service instance:
public interface OnConnectionTimeoutListener {
void onConnectionTimeout();
}
Add an interceptor to your web service:
public WebServiceClient() {
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
client.setReadTimeout(30, TimeUnit.SECONDS);
client.interceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
return onOnIntercept(chain);
}
});
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
webService = retrofit.create(WebService.class);
}
Enclose your intercep code with try-catch block and notify listener when exception happens:
private Response onOnIntercept(Chain chain) throws IOException {
try {
Response response = chain.proceed(chain.request());
String content = UtilityMethods.convertResponseToString(response);
Log.d(TAG, lastCalledMethodName + " - " + content);
return response.newBuilder().body(ResponseBody.create(response.body().contentType(), content)).build();
}
catch (SocketTimeoutException exception) {
exception.printStackTrace();
if(listener != null)
listener.onConnectionTimeout();
}
return chain.proceed(chain.request());
}
Solution 2 - Android
@Override
public void onFailure(Call call, Throwable t) {
if(t instanceof SocketTimeoutException){
message = "Socket Time out. Please try again.";
}
}
Solution 3 - Android
In case someone come here with Kotlin/Coroutines facing the same issue, add an error handler to your coroutines scope:
CoroutineScope(Dispatchers.IO).launch(handler) {
while the handler by itself looks like:
val handler = CoroutineExceptionHandler { _, exception ->
Log.t("Network", "Caught $exception")
}
Solution 4 - Android
None of the answers quite worked for me but they led me in the right direction. See below how I did it in Kotlin.
You can either throw the exceptions in the ErrorInterceptor
and catch them in your api call function:
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
val bodyString = response.body!!.string()
return response.newBuilder()
.body(bodyString.toResponseBody(response.body?.contentType()))
.build()
} catch (e: Exception) {
when (e) {
is SocketTimeoutException -> {
throw SocketTimeoutException()
}
// Add additional errors... //
}
}
}
Or bundle exceptions with a response object; something like this:
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
val bodyString = response.body!!.string()
return response.newBuilder()
.body(bodyString.toResponseBody(response.body?.contentType()))
.build()
} catch (e: Exception) {
var msg = ""
val interceptorCode: Int
when (e) {
is SocketTimeoutException -> {
msg = "Socket timeout error"
interceptorCode = 408
}
// Add additional errors... //
}
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(interceptorCode)
.message(msg)
.body("{${e}}".toResponseBody(null)).build()
}
}
}
Add the ErrorInterceptor
to your okHttpClient
:
okHttpClient.newBuilder()
.addInterceptor(ErrorInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
// ... //
.build()
And then something like this in your repository layer:
suspend fun makeAPIRequest(): Resource<ApiResponse> {
return withContext(ioDispatcher) {
var response: Response<ApiResponse>? = null
try {
response = getResponse()
// Do additional ops on response here //
} catch (e: Exception) {
// Exceptions thrown in ErrorInterceptor will propagate here
}
}
}
Solution 5 - Android
Apparently the exception does get wrapped into a RetrofitException so you can handle it in the failure method.
Solution 6 - Android
It's a bit more complicated. With Retrofit you can make API calls that are either synchronous or asynchronous.
If your endpoint returns void and has a callback it is asynchronous. If it returns something and has no callback it's synchronous.
For asynchronous calls you get this exception in the onFailure(...)
method of your callback.
For synchronous calls you don't get it at all, unless you wrap your call in a try/catch.
try {
// your synchronous call goes here
} catch (RetrofitError error) {
// handle errors
}
Update: The above answer applies to Retrofit 1.9. Retrofit 2.0 has changed this a lot. If you're wondering about how things now work in Retrofit 2.0, this article gives some pointers http://inthecheesefactory.com/blog/retrofit-2.0/en
Solution 7 - Android
I'm posting this for two reasons:
- I personnaly tried increasing connection timeout but, evetually, it doesn't really solve the problem at its root. Besides, user is not supposed to wait for longer than 10 seconds according to this post.
- In a real world application, we would rather do our best to implement a solution in as clean as possible way.
So, here's a solution that I came up with in Kotlin. It's inspired from the answer provided by @Olcay Ertaş and combined with Google's recommended architecture for Android apps.
-
Create a
TimeoutInterceptor
interface:interface TimeoutInterceptor : Interceptor
-
Implement the
TimeoutInterceptor
interface:class TimeoutInterceptorImpl : TimeoutInterceptor { override fun intercept(chain: Interceptor.Chain): Response { if (isConnectionTimedOut(chain)) throw SocketTimeoutException() return chain.proceed(chain.request()) } private fun isConnectionTimedOut(chain: Interceptor.Chain): Boolean { try { val response = chain.proceed(chain.request()) val content = response.toString() response.close() Log.d(tag, "isConnectionTimedOut() => $content") } catch (e: SocketTimeoutException) { return true } return false } }
-
In your
ApiService
interface, add theTimeoutInterceptor
to theOkHttpClient
builder:val okHttpClient = OkHttpClient.Builder() .addInterceptor(requestInterceptor) // Add timeout interceptor .addInterceptor(timeoutInterceptor) // Set a 5s custom connect timout .connectTimeout(5, TimeUnit.SECONDS) .build()
As you might have noticed, you can set a custom connect timeout. Otherwise, it's left to 10 seconds as a default value according to the documentation.
-
Create an enum class
ConnectionState
. It will provide an enum constant objectCONNECTION_TIMEOUT
which will be used further to convey the appropriate connection (or API call) state fromEntityNetworkDataSource
class to the View class (if you follow Google's MVVM pattern):enum class ConnectionState { CONNECTED, NOT_CONNECTED, CONNECTION_TIMEOUT }
-
Assuming your
EntityNetworkDataSource
interface would look something like this:interface EntityNetworkDataSource { val fetchedEntity: LiveData<Entity> // Wrap your ConnectionState object in LiveData in order to be able to observe it in the View val connectionState: LiveData<ConnectionState> // Fetch `Entity` object from the network suspend fun fetchEntity(id: Int) }
-
In the
EntityNetworkDataSource
implementation class, you can properly catch theSocketTimeoutException
as shown below, inside thefetchEntity(id: Int)
implementation:class EntityNetworkDataSourceImpl( private val apiService: ApiService ) : EntityNetworkDataSource { private val _fetchedEntity = MutableLiveData<Entity>() override val fetchedEntity: LiveData<Entity> get() = _fetchedEntity // We want to keep our MutableLiveData private because they can be changed. // So we want to be able to update them only from the inside of this class private val _connectionState = MutableLiveData<ConnectionState>() override val connectionState: LiveData<ConnectionState> get() = _connectionState override suspend fun fetchEntity(id: Int) { try { val fetchedEntity = apiService .getEntity(id) .await() // Convey the updated connection state to the observer View _connectionState.postValue(ConnectionState.CONNECTED) _fetchedEntity.postValue(fetchedEntity) } catch (e: SocketTimeoutException) { Log.e(tag, "Connection timeout. ", e) // Catch the SocketTimeoutException and post the updated connection state to the observer View _connectionState.postValue(ConnectionState.CONNECTION_TIMEOUT) } } }
The same principle applies to any connection exception you wana intercept and catch.
Solution 8 - Android
Kotlin
If you want to use Retrofit
in Kotlin
follow below steps:
Define your Retrofit interface:
interface GitHubApi {
@GET("/users/{userName}/repos")
fun repos(@Path("userName") userName: String): Call<List<Repo>>
}
Implement your service:
class Api(...) {
private val baseUrl = "https://api.github.com"
private val api: GitHubApi
private fun loggingInterceptor(...): HttpLoggingInterceptor {...}
private fun okHttpBuilder(): OkHttpClient {...}
init {...}
fun repos(
userName: String,
onSuccess: (list: List<Repo>?) -> Unit,
onFailure: (message: String?) -> Unit): Future<Unit> {
return runAsync(api.repos(userName), onSuccess, onFailure)
}
private fun <T> runAsync(
call: retrofit2.Call<T>,
onSuccess: (T?) -> Unit,
onFailure: (message: String?) -> Unit) : Future<Unit> {
return doAsync {
try {
val response = call.execute()
when {
response.isSuccessful -> response.body()?.let {
onSuccess(it)
}
else -> {
onFailure(response.raw().message())
}
}
} catch (e: IOException) {
if (e is SocketTimeoutException) {
onFailure("Response time out!")
} else {
onFailure(e.message)
}
}
}
}
}
Call your service in where you want:
Api().repos("olcayertas",
onSuccess = {
Log.d("MainActivity", "Response:\n" + toJson(it))
},
onFailure = {
Log.e("MainActivity", "Error: $it")
})
You can handle any exception you want in runAsync
function.
You can get fully working example here.