web-dev-qa-db-ja.com

Kotlin Android Retrofit 2.6.0 withコルーチンエラー処理

Retrofit 2.6.0とコルーチンを使用して、Webサービスを呼び出しています。すべての応答コード(成功およびエラーの場合)でAPI応答を適切に取得しています。私の問題は、API呼び出しの間にインターネット(Wifi /モバイルデータ)を切断すると、作成したコードからエラーが正しくキャッチされないことです。ほとんどの場合、エラーはConnectExceptionとSocketExceptionです。

私はインターセプターを使用して、また私の呼び出しを開始したViewModelからもエラーをキャッチしようとしました。しかし、ここでも、例外は捕捉されて処理されていません。

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

スタックトレース:

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_

まあ、それは私がやっていることです、トライキャッチキャッチジャンクコピーペーストを減らすためだけに

このようなAPI呼び出しメソッドを宣言します

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

別のファイル

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

使用法:

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

HTTPコードはRetrofitによって処理されます-responseCodeが2xxでない場合、HttpExceptionをスローするだけです。したがって、私たちがすべきことは、この例外をキャッチすることです。

私は知っています、それは完璧な解決策ではありませんが、ジェイクにもっと良いものを発明させましょう

8
Arthur Matsegor

CoroutineExceptionHandlerを追加するだけで、エラーを処理できます。

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

多分それは誰かを助ける:次の方法でSocketTimeoutExceptionを取り除くことが可能です:1.クライアントのreadTimeoutを任意の数に設定します。ここでは2sです

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

2. API呼び出しを行うときは、常にコルーチンタイムアウト内にラップします。

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

重要な点は、withTimeout値がRetrofitタイムアウト値より小さいことです。これにより、レトロフィットタイムアウトが発生する前に、コルーチンの中断が停止されます。

とにかく、これは多くのtry/catchブロックを生成しますが、コルーチンのサポートを含めると、レトロフィットの開発者が望んだものではないでしょう。

0

Arthur Matsegorの回答への追加:

私の場合、APIは不正なリクエストに対してエラーメッセージを返します。このシナリオでは、Catch関数でエラーメッセージをキャッチする必要があります。 Catch関数にtry/catchを書くのは醜いように見えますが、うまくいきました。

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