63

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?

starball
  • 20,030
  • 7
  • 43
  • 238
Jdruwe
  • 3,450
  • 6
  • 36
  • 57
  • In my app, I do get SocketTimeoutExceptions passed to my failure method wrapped in RetrofitExceptions. Are you sure that you're not getting them there? They may also be logged elsewhere, but they should be passed to failure(). – GreyBeardedGeek Apr 28 '15 at 14:08
  • Hmm very weird, I did some debugging and now it does work :S. – Jdruwe Apr 28 '15 at 14:17
  • 1
    Hi jdruwe, could you manage to catch the connection exception? I too want to do the same. Thanks – cgr Nov 18 '15 at 08:18
  • https://stackoverflow.com/a/56301801/7254873 – Sumit Shukla May 25 '19 at 04:51

7 Answers7

61

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());
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
  • 2
    This interceptor is great, but whe TimeoutException occurs in my subscriber's onError(Throwable t) i get this: t.getMessage() = "network interceptor com.test.presentation.application.di.AppRestModule$2@2320fd48 must call proceed() exactly once". Is that OK? Or maybe we should not call proceed second time? – Illia K. Mar 29 '16 at 08:01
  • 1
    @Olcay Ertaş - Great answer! Curious on how you get/set lastCalledMethodName for your logging, would you mind sharing? – Codeversed Dec 27 '16 at 19:25
  • @Codeversed, my web service methods assigns this value. There is no concurrency for this methods so it doesn't cause any problem. But i guess adding method name to request would be better for concurrent requests. – Olcay Ertaş Dec 28 '16 at 08:55
  • I think its changed with `httpClient.connectTimeout(10, TimeUnit.SECONDS); httpClient.readTimeout(30, TimeUnit.SECONDS);` – Pratik Butani Feb 09 '17 at 06:23
  • 1
    @OlcayErtaş chain.proceed(chain.request()) will be called two times! Is it ok? – User Apr 20 '17 at 12:56
  • 4
    Where does `UtilityMethods.convertResponseToString(response);` come from? – raisedandglazed Apr 20 '17 at 22:07
  • It is my implementation. – Olcay Ertaş Apr 21 '17 at 06:49
  • @OlcayErtaş Does this solve anything? What you catch here is already thrown. When I get this exception, the app crashes because it is raised in OkHttp: https://github.com/square/okhttp/issues/3320 – Herrbert74 Jun 09 '17 at 11:42
  • Yes it does. Did you try my soulition or are just asking before trying it? – Olcay Ertaş Jun 09 '17 at 12:33
  • 2
    UtilityMethods.convertResponseToString(response) - what does this exactly do? – Alberto M Nov 15 '17 at 15:41
  • Alberto, it does exactly what it says, converts Response object to String. – Olcay Ertaş Nov 16 '17 at 07:22
  • why not just check the throwable instance to see if it is a SocketTimeoutException in the failure route? – hjchin Jul 14 '18 at 02:41
  • 1
    Thanks! @rpm, it is `response.body().string()` (see https://stackoverflow.com/a/37118725/2914140). – CoolMind Feb 21 '19 at 19:25
  • 1
    @hjchin, if you use RxJava or Kotlin coroutines, there is no `onFailure` branch. So, you can try it only in callbacks. – CoolMind Feb 21 '19 at 19:30
  • This answer fails. `return chain.proceed(chain.request());` returns `java.net.SocketTimeoutException`. Also the author doesn't check `UnknownHostException`. – CoolMind Feb 22 '19 at 08:14
  • This method is to catch `SocketTimeoutException` not `UnknownHostException`. – Olcay Ertaş Feb 22 '19 at 08:39
  • @OlcayErtaş, agree. I saw many topics with interceptors. They all rethrow exceptions to upper level, so we have to catch them in callbacks, try-catch or RxJava. Your answer is good. We can catch different Network exceptions, not only timeout. Also thanks for getting JSON in `content` variable. Here I can change it without writing a Gson deserializer. – CoolMind Feb 22 '19 at 16:05
  • response.newBuilder().body(ResponseBody.create(response.body().contentType(), content)).build(); why this line? , we do not change anything ? – ASA Oct 18 '20 at 09:23
  • ok good the catch recevie the excetion but last line return chain.proceed(chain.request()); throw exception – ASA Oct 29 '20 at 07:39
  • @OlcayErtaş check my last comment plz – ASA Oct 29 '20 at 08:38
  • @asa it has been very long time since I have last used Retrofit and do any coding on Android. I can't remember why I have used it. – Olcay Ertaş Oct 29 '20 at 19:43
  • @asa please check this: https://square.github.io/okhttp/interceptors/ – Olcay Ertaş Oct 29 '20 at 19:44
  • @asa I am developing native iOS applications for last 4 years. – Olcay Ertaş Nov 01 '20 at 10:47
54
@Override
public void onFailure(Call call, Throwable t) {
    if(t instanceof SocketTimeoutException){
        message = "Socket Time out. Please try again.";
    }
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Bhavya V
  • 539
  • 4
  • 5
18

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")
}
careful7j
  • 799
  • 9
  • 20
  • Yup this is correct for Kotlin coroutines. Even if you put your whole code within a try catch it will not catch the exception if you have used a coroutine. In that case this is the correct approach. – nuwancy Sep 09 '22 at 11:04
6

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

            }
        }
    }
SVP
  • 2,773
  • 3
  • 11
  • 14
  • hey, is this working for you full-proof?. I tried it but still, 50% of the time exception is skipped by the interceptor and not caught in case of multiple async API calls. – Arpit J. Oct 02 '21 at 03:16
  • @Arpit J. is the interceptor even processing the chain when it does not catch errors? I haven't used this error interceptor for multiple async calls but I would start investigating there. If it does not process the chain, then it's something that has to do with how OkHttp interceptors work, otherwise it has something to do with my implementation. If you manage to solve you issue, I'd be curious to know what was wrong and how you fixed it. Thanks and good luck! – SVP Oct 02 '21 at 03:34
  • Sorry but I'm swamped already. If this is an interceptor issue, I don't have any clue how to fix. You should look into something like this: https://github.com/square/okhttp/issues/3714 – SVP Oct 02 '21 at 04:51
3

Apparently the exception does get wrapped into a RetrofitException so you can handle it in the failure method.

Jdruwe
  • 3,450
  • 6
  • 36
  • 57
  • 1
    I am using Retrofit 2.2. May I know how can I catch the RetrofitException ? I have an interceptor surrounded by try catch so I am getting SocketTimeoutException handled. But problem is how to let my CallBack know that this particular error is because of the connection timeout ? – cgr Nov 30 '15 at 15:56
  • @cgr please llok at my answer. – Olcay Ertaş Dec 04 '15 at 14:57
  • @Oclay, thansk for the response. I finally did that way. Thanks ! ;) – cgr Dec 04 '15 at 14:59
3

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

Espen Riskedal
  • 1,425
  • 15
  • 28
1

I'm posting this for two reasons:

  1. 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.
  2. 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.

  1. Create a TimeoutInterceptor interface:

     interface TimeoutInterceptor : Interceptor
    
  2. 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
         }
     }
    
  3. In your ApiService interface, add the TimeoutInterceptor to the OkHttpClient 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.

  1. Create an enum class ConnectionState. It will provide an enum constant object CONNECTION_TIMEOUT which will be used further to convey the appropriate connection (or API call) state from EntityNetworkDataSource class to the View class (if you follow Google's MVVM pattern):

     enum class ConnectionState {
         CONNECTED, NOT_CONNECTED, CONNECTION_TIMEOUT
     }
    
  2. 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)
     }
    
  3. In the EntityNetworkDataSource implementation class, you can properly catch the SocketTimeoutException as shown below, inside the fetchEntity(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.

AndroWeed
  • 133
  • 2
  • 12