web-dev-qa-db-fra.com

Kotlin Android Retrofit 2.6.0 avec gestion des erreurs coroutines

J'utilise Retrofit 2.6.0 avec des coroutines pour mon appel de service Web. Je reçois correctement la réponse de l'API avec tous les codes de réponse (cas de réussite et d'erreur). Mon problème est que lorsque je déconnecte Internet (données Wifi/mobile) entre un appel API, à partir du code que j'ai écrit, l'erreur ne se rattrape pas correctement. La plupart du temps, les erreurs sont ConnectException et SocketException.

J'ai essayé de capturer l'erreur en utilisant l'intercepteur et également depuis le ViewModel où j'ai également initié mon appel. mais ici aussi, l'exception n'est pas prise et manipulée.

//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()
    }
}

La trace de la pile:

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)
11
shaheer_

Eh bien, c'est ce que je fais, juste pour réduire le copypaste indésirable

Déclarez nos méthodes d'appel API comme ceci

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

Dans un fichier séparé

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() }
}

Les codes HTTP sont gérés par Retrofit - il lève juste une HttpException si responseCode n'est pas 2xx. Donc, ce que nous devons faire, c'est simplement attraper cette exception.

Je sais, ce n'est pas une solution parfaite, mais laissons Jake inventer quelque chose de mieux)

8
Arthur Matsegor

Vous pouvez simplement ajouter un CoroutineExceptionHandler pour gérer l'erreur à votre place:

Dans votre 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
        }
}
1

Peut-être que cela aide quelqu'un: Il est possible de se débarrasser de SocketTimeoutException de la manière suivante: 1. Définissez le readTimeout de votre client sur un nombre arbitraire, ici ses 2

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

2. Lorsque vous effectuez des appels API, enveloppez-les toujours dans un délai d'expiration coroutine.

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

Le point principal est que la valeur withTimeout est inférieure à la valeur de délai d'expiration Retrofit. Cela garantit que la coroutine cesse d'être suspendue AVANT que le délai de mise à niveau ne se déclenche.

Quoi qu'il en soit, cela produit de nombreux blocs try/catch et n'est probablement pas ce que les développeurs de modernisation voulaient, en incluant le support de la coroutine.

0
8K Creative Commons

Un ajout à la réponse d'Arthur Matsegor:

Dans mon cas, l'API me renvoie un message d'erreur pour les mauvaises demandes. Pour ce scénario, j'ai besoin d'attraper un message d'erreur sur la fonction Catch. Je sais, écrire la fonction try/catch dans Catch a l'air moche mais ça a marché.

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
    }
}
0
Mete