shaheer92
shaheer92

Reputation: 415

Kotlin Android Retrofit 2.6.0 with coroutines error handling

I am using Retrofit 2.6.0 with coroutines for my web service call. I am getting the API response properly with all response codes (success & error cases). My Issue is when I disconnect the internet(Wifi/mobile data) in between an API call, from the code that I have written, the error is not getting caught properly. The errors most of the time are ConnectException and SocketException.

I have tried to catch the error using interceptor and also in from the ViewModel where initiated my call as well. but here as well, the exception is not getting caught and handled.

//ApiService
@GET(ApiUrl.API_DASHBOARD)
    suspend fun getHomeUiDetails(@Header("Authorization") authHeader: String): Response<HomeDetailsResponse>

//ConnectionBridge
suspend fun getHomeUiDetails(authToken: String): Response<HomeDetailsResponse> {
        return ApiServiceGenerator.BASIC_CLIENT_CONTRACT.getHomeUiDetails(authToken)
    }

// ViewModel
viewModelScope.launch(Dispatchers.IO) {
   val apiResponse = ApiConnectionBridge.getHomeUiDetails(SharedPrefUtils.getAuthToken(context))
        if (apiResponse.isSuccessful) {
           // success case
        } else {
            // error case
        }
}

object ApiServiceGenerator {

    val BASIC_CLIENT_CONTRACT: ApiService = ApiClient
        .getContract(
            ApiService::class.java,
            true,
            BuildConfig.BASE_URL
        )
}

object ApiClient {

    fun <T> getContract(clazz: Class<T>, isAuth: Boolean, baseUrl: String): T {
        return getRetrofitBuilder(baseUrl, getContractBuilder(isAuth)).create(clazz)
    }

    private fun getRetrofitBuilder(baseUrl: String, builder: OkHttpClient.Builder): Retrofit {
        val gson = GsonBuilder().serializeNulls().create()

        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY

        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor { chain ->
                val original = chain.request()

                // Customize the request
                val request = original.newBuilder()
                request.header("Content-Type", "application/x-www-form-urlencoded")

                var response: Response? = null
                try {
                    response = chain.proceed(request.build())
                    response.cacheResponse()

                    // Customize or return the response

                    response!!
                } catch (e: ConnectException) {
                            Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
                    chain.proceed(original)
                } catch (e: SocketException) {
                    Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
                    chain.proceed(original)
                } catch (e: IOException) {
                    Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
                    chain.proceed(original)
                } catch (e: Exception) {
                    Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
                    chain.proceed(original)
                }
            }
            //            .cache(cache)
            .eventListener( object : EventListener() {
                override fun callFailed(call: Call, ioe: IOException) {
                    super.callFailed(call, ioe)
                }

            })
            .addInterceptor(loggingInterceptor)
            .readTimeout(60, TimeUnit.SECONDS)
            .connectTimeout(60, TimeUnit.SECONDS)
            .build()

        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient)//getUnsafeOkHttpClient()
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build()
    }
}

The stack trace:

2019-08-02 14:15:12.819 4157-4288/com.my.app E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
Process: com.my.app, PID: 4157
java.net.ConnectException: Failed to connect to my_url
    at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:248)
    at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:166)
    at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:257)
    at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135)
    at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114)
    at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:42)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
    at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
    at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:126)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
    at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:213)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
    at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
    at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254)
    at okhttp3.RealCall$AsyncCall.execute(RealCall.java:200)
    at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:764)
 Caused by: java.net.ConnectException: failed to connect to my_url (port 80) from /:: (port 0) after 60000ms: connect failed: ENETUNREACH (Network is unreachable)
    at libcore.io.IoBridge.connect(IoBridge.java:137)
    at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:137)
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:390)
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:230)
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:212)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:436)
    at java.net.Socket.connect(Socket.java:621)
    at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.java:73)
    at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:246)
    at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:166) 
    at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:257) 
    at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135) 
    at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114) 
    at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:42) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:126) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:213) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254) 
    at okhttp3.RealCall$AsyncCall.execute(RealCall.java:200) 
    at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) 
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
    at java.lang.Thread.run(Thread.java:764) 
 Caused by: android.system.ErrnoException: connect failed: ENETUNREACH (Network is unreachable)
    at libcore.io.Linux.connect(Native Method)
    at libcore.io.BlockGuardOs.connect(BlockGuardOs.java:118)
    at libcore.io.IoBridge.connectErrno(IoBridge.java:168)
    at libcore.io.IoBridge.connect(IoBridge.java:129)
    at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:137) 
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:390) 
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:230) 
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:212) 
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:436) 
    at java.net.Socket.connect(Socket.java:621) 
    at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.java:73) 
    at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:246) 
    at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:166) 
    at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:257) 
    at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135) 
    at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114) 
    at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:42) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:126) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:213) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147) 
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121) 
    at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:254) 
    at okhttp3.RealCall$AsyncCall.execute(RealCall.java:200) 
    at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) 
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
    at java.lang.Thread.run(Thread.java:764)

Upvotes: 28

Views: 24637

Answers (7)

Vladimir Petrovski
Vladimir Petrovski

Reputation: 2096

You don't need interceptors for this problem. Wrap the retrofit API call with try/catch like this:

suspend fun getHomeUiDetails(authToken: String): Result<HomeDetailsResponse>
        return try {
            val response = yourRetrofitService.getHomeUiDetails(authToken)

            if (response.isSuccessful) {
                return Result.Success(response)
            }

            return Result.Error(
                IOException("Error getting details ${response.code()} ${response.message()}")
            )
        } catch (e: Exception) {
            Result.Error(IOException("Error getting details", e))
        }
}

Upvotes: -2

Kishan Solanki
Kishan Solanki

Reputation: 14636

I am a bit late to the party but I think this is the finest solution:

If you are using Kotlin + Retrofit + Coroutines then just use try and catch for network operations like,

viewModelScope.launch(Dispatchers.IO) {
        try {
            val userListResponseModel = apiEndPointsInterface.usersList()
            returnusersList(userListResponseModel)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

Where, Exception is type of kotlin and not of java.lang

This will handle every exception like,

  1. HttpException
  2. SocketTimeoutException
  3. FATAL EXCEPTION: DefaultDispatcher etc

Here is my usersList() function

@GET(AppConstants.APIEndPoints.HOME_CONTENT)
suspend fun usersList(): UserListResponseModel

Note: Your RetrofitClient Class must have this as client

OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)

Upvotes: 3

chikwapuro
chikwapuro

Reputation: 1354

you could do the following in your ViewModel to gracefully handle exceptions:

viewModelScope.launch {
   kotlin.runCatching {
      withContext(Dispatchers.IO){
          ApiConnectionBridge.getHomeUiDetails(SharedPrefUtils.getAuthToken(context))
      }

  }.onSuccess { 
       // do something with success response
  }.onFailure{
      // do something on failure response
  }

}

Upvotes: 1

8K Creative Commons
8K Creative Commons

Reputation: 253

Maybe it helps somebody: Its possible to get rid of the SocketTimeoutException in the following way: 1. Set the readTimeout of your client to a arbitrary number, here its 2s

 val client = OkHttpClient.Builder()
            .connectTimeout(1, TimeUnit.SECONDS)
            .readTimeout(2, TimeUnit.SECONDS).build()

2. When doing API calls always wrap them inside a coroutine timeout.

  try {
          withTimeout(1000) {
              try {
                  val retrivedTodo = APICall()
                  emit(retrivedTodo)
              } catch (exception: HttpException) {
                  exception.printStackTrace()
              }
          }
      }catch (ex: CancellationException) {
        Log.e("timeout","TimeOut")
      }

The main point is, that the withTimeout value is smaller than the Retrofit timeout value. This assures, that the coroutine stops being suspended BEFORE the Retrofit timeout kicks in.

Anyway, this is produces many try/catch blocks and is probably not what the retrofit developers wanted, when including the coroutine support.

Upvotes: 0

Mete
Mete

Reputation: 2884

An addition to Arthur Matsegor's answer:

In my case API returns to me an error message for bad requests. For this scenario, i need to catch error message on Catch function. I know, writing try/catch to Catch function looks like ugly but it's worked.

private suspend fun <T : Any> handleRequest(requestFunc: suspend () -> T): Result<T> {
    return try {
        Result.success(requestFunc.invoke())
    } catch (httpException: HttpException) {
        val errorMessage = getErrorMessageFromGenericResponse(httpException)
        if (errorMessage.isNullOrBlank()) {
            Result.failure(httpException)
        } else {
            Result.failure(Throwable(errorMessage))
        }
    }
}

private fun getErrorMessageFromGenericResponse(httpException: HttpException): String? {
    var errorMessage: String? = null
    try {
        val body = httpException.response()?.errorBody()
        val adapter = Gson().getAdapter(GenericResponse::class.java)
        val errorParser = adapter.fromJson(body?.string())
        errorMessage = errorParser.errorMessage?.get(0)
    } catch (e: IOException) {
        e.printStackTrace()
    } finally {
        return errorMessage
    }
}

Upvotes: 2

apksherlock
apksherlock

Reputation: 8371

You can just add a CoroutineExceptionHandler to handle the error for you:

In your ViewModel:

val coroutineExceptionHandler = CoroutineExceptionHandler{_, t -> {
  t.printStackTrace()
  showErrorOrSomething()
}}

viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
   val apiResponse = ApiConnectionBridge.getHomeUiDetails(SharedPrefUtils.getAuthToken(context))
        if (apiResponse.isSuccessful) {
           // success case
        } else {
            // error case
        }
}

Upvotes: 6

Artur Matsehor
Artur Matsehor

Reputation: 221

Well, that's what I do, just to reduce try-catch junk copypaste

Declare our API call methods like this

@GET("do/smth")
suspend fun doSomething(): SomeCustomResponse

In a separate file

suspend fun <T: Any> handleRequest(requestFunc: suspend () -> T): kotlin.Result<T> {
    return try {
        Result.success(requestFunc.invoke())
    } catch (he: HttpException) {
        Result.failure(he)
    }
}

Usage:

suspend fun doSmth(): kotlin.Result<SomeCustomResponse> {
    return handleRequest { myApi.doSomething() }
}

HTTP codes are handled by Retrofit - it just throws an HttpException if responseCode is not 2xx. So what we should do is just catch this exception.

I know, it is not a perfect solution, but let's for Jake to invent something better)

Upvotes: 19

Related Questions