处理 Kotlin Coroutines 中自定义 okhttp 拦截器引发的异常

Posted

技术标签:

【中文标题】处理 Kotlin Coroutines 中自定义 okhttp 拦截器引发的异常【英文标题】:Handle exceptions thrown by a custom okhttp Interceptor in Kotlin Coroutines 【发布时间】:2020-03-01 00:10:49 【问题描述】:

我在我的 android 应用程序中使用自定义 Interceptor 和 Retrofit 客户端,在某些特定情况下会引发异常。我正在尝试使用 Kotlin 协程使其工作。

问题是我无法处理前面提到的错误,因为在从 Interceptor 实例中抛出异常的那一刻,它会使整个应用程序崩溃,而不是被协程的 try/catch 语句捕获。当我使用 Rx 实现时,异常被完美地传播到 onError 回调中,我能够以我需要的方式处理它。

我猜这与用于网络调用的底层线程有某种关系,请查看下面的日志,从调用的地方,从抛出异常之前的拦截器,以及堆栈跟踪:

2019-11-04 17:17:34.515 29549-29729/com.app W/TAG: Running thread: DefaultDispatcher-worker-1
2019-11-04 17:17:45.911 29549-29834/com.app W/TAG: Interceptor thread: OkHttp https://some.endpoint.com/...

2019-11-04 17:17:45.917 29549-29834/com.app E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.app, PID: 29549
    com.app.IllegalStateException: Passed refresh token can\'t be used for refreshing the token.
        at com.app.net.AuthInterceptor.intercept(AuthInterceptor.kt:33)

我应该怎么做才能正确地从拦截器捕获和处理这个异常?我错过了什么吗?

【问题讨论】:

“我猜这在某种程度上与用于网络调用的底层线程有关”——你是说在 Rx 实现中,你所拥有的“运行线程”和“拦截线程”是一样的吗?您可能想要编辑您的问题并提供minimal reproducible example,显示拦截器代码和未处理的自定义异常的完整堆栈跟踪。 只有IOException 被传播给调用者。所有其他异常都委托给未捕获的异常处理程序。这意味着您应该抛出IOException,而不是抛出IllegalStateException 【参考方案1】:

您应该继承 IOException 并使用它来将信息从您的拦截器发送到您的调用代码。

我们将IllegalStateException 等其他异常视为应用程序崩溃,并且不会将它们发送到线程边界,因为我们不想让大多数调用者承担捕获它们的负担。

【讨论】:

这似乎是该问题最干净的解决方案。我只是想知道为什么在 Rx 实现中任何异常都被转发给调用者。 Retrofit 的 Rx 适配器内部有什么东西吗? TBH 我发现这是一个非常糟糕的设计决定。为什么您认为您可以为用户决定他们是否要“承担”捕获更通用异常的负担?为什么我的自定义异常被强制为 IOException,即使它们在语义上不是? @fyodor 这里是对这个设计决策的讨论:github.com/square/okhttp/pull/5457 @JesseWilson 从什么时候开始?这让我浪费了半天时间!为什么?对我来说毫无意义。 IOException = 网络问题。我使用该异常来决定何时在网络恢复联机时自动重试。我已经创建了一个关于它的问题github.com/square/retrofit/issues/3505【参考方案2】:

您可以在自定义 Interceptor 中捕获异常,并返回带有特定 messagecode 的空响应。我已经实现了一个自定义Interceptor 来处理诸如当您没有或互联网连接缓慢等情况......实际上协程的挂起函数在处理网络调用时会抛出异常。根据我的经验,您可以遵循两种方法。 1. 将您的所有网络调用封装在 try...catch2. 创建自定义 Interceptor 并在那里处理异常并返回一些特定的响应。

方法一:

try 
    webservice.login(username, password)
 catch (e: Exception) 
    //...

方法2:

创建一个自定义Interceptor 并在那里处理异常。

class LoggingInterceptor : Interceptor 

   @Throws(Exception::class)
   override fun intercept(chain: Interceptor.Chain): Response 
      val request = chain.request()
      try 
          val response = chain.proceed(request)
        
          val bodyString = response.body()!!.string()

          return response.newBuilder()
            .body(ResponseBody.create(response.body()?.contentType(), bodyString))
            .build()
       catch (e: Exception) 
          e.printStackTrace()
          var msg = ""
          when (e) 
             is SocketTimeoutException -> 
                msg = "Timeout - Please check your internet connection"
             
             is UnknownHostException -> 
                msg = "Unable to make a connection. Please check your internet"
             
             is ConnectionShutdownException -> 
                msg = "Connection shutdown. Please check your internet"
             
             is IOException -> 
                msg = "Server is unreachable, please try again later."
             
             is IllegalStateException -> 
                msg = "$e.message"
             
             else -> 
                msg = "$e.message"
             
          

          return Response.Builder()
                .request(request)
                .protocol(Protocol.HTTP_1_1)
                .code(999)
                .message(msg)
                .body(ResponseBody.create(null, "$e")).build()
      
   

我已经创建了完整实现LoggingInterceptor 的要点,并带有请求和响应的打印日志。 LoggingInterceptor

【讨论】:

感谢您的Approach 2。太有帮助了! 天哪,整个互联网上最好的方法。【参考方案3】:

我不知道你到底需要什么,但这样理解:

    OkHttpClient okHttpClient = new OkHttpClient.Builder()  
        .addInterceptor(new Interceptor() 
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException 
                Request request = chain.request();
                okhttp3.Response response = chain.proceed(request);

                // todo deal with the issues the way you need to
                if (response.code() == SomeCode) 
                   //do something
                    return response;
                

                return response;
            
        )
        .build();

Retrofit.Builder builder = new Retrofit.Builder()  
        .baseUrl(url)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create());

Retrofit retrofit = builder.build();  

【讨论】:

以上是关于处理 Kotlin Coroutines 中自定义 okhttp 拦截器引发的异常的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin Coroutines:等待多个线程完成

Spring WebFlux Reactive 和 Kotlin Coroutines 启动错误

链接 ktor 和 kotlinx.coroutines 会导致未定义的符号

有没有办法在 Kotlin 中使用 coroutines/Flow/Channels 实现这个 rx 流?

Kotlin Coroutines 协程实现原理全解析

Kotlin 协程源码解析