Kotlin Android Retrofit 2.6.0与协程错误处理

28
我正在使用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:764Caused 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)

5
你解决了这个问题吗? - Vladimir Petrovski
7个回答

19

我所做的就是为了减少try-catch冗余代码的复制粘贴

像这样声明我们的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处理 - 如果响应代码不是2xx,它会抛出HttpException。所以我们需要做的就是捕获这个异常。 我知道,这不是一个完美的解决方案,但让我们等待Jake发明更好的东西吧:)

如果结果是一个 Unit 呢? - StuartDTO
1
谢谢Arthur,它起作用了。但是我的API的通用响应包含错误消息,我想获取错误主体。我如何使用Result.failure返回此消息,是否可能? - Mete
1
不错的想法,@Arthur。但是Jetbrain的Kotlin似乎有一些限制。只需查看此链接https://dev59.com/qVQK5IYBdhLWcg3wBrQK - Raghul Vaikundam
只有在 HttpException 的情况下才会抛出异常吗? - IgorGanapolsky
1
如果您只想在Result中包装try/catch,则有一个内置的runCatching()函数。 ;) - gmk57
显示剩余5条评论

6
您可以添加一个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
        }
}

你能告诉我一些解决方法来获取这种方法中的错误代码和错误信息吗?我会非常感激。 - Hossein Farrokhi
apiResponse 应该具有它。也许是 body()errorBodyapiResponse.code - coroutineDispatcher

2
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
    }
}

1

在您的ViewModel中,您可以采取以下措施以优雅地处理异常:

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

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

}

这个不起作用。你在发帖之前试过吗? - Kishan Solanki
这个对我有效✅,但是我如何获取来自服务器的错误消息? - Dharamveer Mithilesh Gupta
我的API服务器返回404时的JSON响应为{"success": 0, "error": "数据未找到。"}。 - Dharamveer Mithilesh Gupta

1
如果你正在使用Kotlin + Retrofit + Coroutines,那么这是最好的解决方案:对于网络操作,只需使用try和catch。
viewModelScope.launch(Dispatchers.IO) {
        try {
            val userListResponseModel = apiEndPointsInterface.usersList()
            returnusersList(userListResponseModel)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

在这里,Exception是kotlin类型而不是java.lang类型。

这将处理每个异常,例如:

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

这是我的usersList()函数。

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

注意: 你的RetrofitClient类必须将此作为client

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

但这是检查网络操作的正确方式吗?只是确认一下...我们不能使用 withContext(Dispatchers.Main) { //toast for fail } 吗?需要解释...谢谢。 - Wini
@Wini,你可以使用sealed类来处理响应,无论是错误还是成功。 - Kishan Solanki
不要捕获“Exception”,因为它也会捕获“CancellationException”,而这对于协程在作用域中的正常作业关闭传播是必需的。请捕获特定的异常。 - Christopher Perry

0
也许对某些人有帮助: 可以通过以下方式摆脱SocketTimeoutException: 1. 将客户端的readTimeout设置为任意数字,这里是2秒。
 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 的超时时间。这可以确保,在 Retrofit 超时启动之前,协程停止被挂起。
无论如何,这会产生许多 try/catch 块,而且可能不是 Retrofit 开发人员在包含协程支持时想要的。

这不是解决方案。 - Kishan Solanki

-2

对于这个问题,您不需要拦截器。可以像这样使用try/catch包装Retrofit API调用:

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

在 Kotlin 1.3.40 中,Result 类中没有名为 Error 的函数。因此,我无法在这里使用该方法。 - shaheer_

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接