【问题标题】:Retrofit 2: Catch connection timeout exception改造2:捕捉连接超时异常
【发布时间】:2015-07-07 10:28:30
【问题描述】:

我有以下设置:

final OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setReadTimeout(5, TimeUnit.SECONDS);
okHttpClient.setConnectTimeout(5, TimeUnit.SECONDS);

RestAdapter.Builder builder = new RestAdapter.Builder()
        .setEndpoint(ROOT)
        .setClient(new OkClient(okHttpClient))
        .setLogLevel(RestAdapter.LogLevel.FULL);

我正在尝试处理我的服务器关闭并且用户收到连接超时异常的情况,这是我的日志记录:

java.net.SocketTimeoutException: failed to connect to /192.168.0.53 (port 3000) after 5000ms

完整记录:http://pastebin.com/gscCGb7x

有没有办法将这个路由到改造故障方法中,以便我可以在那里处理它?

提前致谢!

【问题讨论】:

  • 在我的应用程序中,我确实将 SocketTimeoutExceptions 传递给了包含在 RetrofitExceptions 中的失败方法。你确定你没有把它们送到那里吗?它们也可能记录在其他地方,但应该传递给 failure()。
  • 嗯很奇怪,我做了一些调试,现在它可以工作了:S.
  • 嗨 jdruwe,你能设法捕捉到连接异常吗?我也想做同样的事情。谢谢

标签: android retrofit connection-timeout okhttp


【解决方案1】:

用于改造 2

在您的网络服务实例中定义一个监听器:

public interface OnConnectionTimeoutListener {
    void onConnectionTimeout();
}

为您的网络服务添加一个拦截器:

public WebServiceClient() {
    OkHttpClient client = new OkHttpClient();
    client.setConnectTimeout(10, TimeUnit.SECONDS);
    client.setReadTimeout(30, TimeUnit.SECONDS);
    client.interceptors().add(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            return onOnIntercept(chain);
        }
    });
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build();
    webService = retrofit.create(WebService.class);
}

用 try-catch 块封闭您的拦截代码,并在发生异常时通知侦听器:

private Response onOnIntercept(Chain chain) throws IOException {
    try {
        Response response = chain.proceed(chain.request());
        String content = UtilityMethods.convertResponseToString(response);
        Log.d(TAG, lastCalledMethodName + " - " + content);
        return response.newBuilder().body(ResponseBody.create(response.body().contentType(), content)).build();
    }
    catch (SocketTimeoutException exception) {
        exception.printStackTrace();
        if(listener != null)
            listener.onConnectionTimeout();
    }

    return chain.proceed(chain.request());
}

【讨论】:

  • 这个拦截器很棒,但是当我的订阅者的 onError(Throwable t) 发生 TimeoutException 时,我得到了这个: t.getMessage() = "network interceptor com.test.presentation.application.di.AppRestModule$2 @2320fd48 必须只调用一次proceed()”。那样可以么?或者我们不应该第二次调用proceed?
  • @Olcay Ertaş - 很好的答案!好奇您如何获取/设置 lastCalledMethodName 进行日志记录,您介意分享一下吗?
  • @OlcayErtaş chain.proceed(chain.request()) 将被调用两次!没事吧?
  • UtilityMethods.convertResponseToString(response); 来自哪里?
  • UtilityMethods.convertResponseToString(response) - 这究竟是做什么的?
【解决方案2】:
@Override
public void onFailure(Call call, Throwable t) {
    if(t instanceof SocketTimeoutException){
        message = "Socket Time out. Please try again.";
    }
}

【讨论】:

  • 能否告诉我如何处理连接异常?
  • 对我来说可以抛出 UnknownHostException 实例!
  • 所以你说onResponse中没有超时时间?
  • @Kenji 从我的测试中,它似乎总是去 onFailure
【解决方案3】:

如果有人带着 Kotlin/Coroutines 来到这里遇到同样的问题,请将错误处理程序添加到您的协程范围:

CoroutineScope(Dispatchers.IO).launch(handler) {

而处理程序本身看起来像:

val handler = CoroutineExceptionHandler { _, exception ->
    Log.t("Network", "Caught $exception")
}

【讨论】:

    【解决方案4】:

    显然异常确实被包装到 RetrofitException 中,因此您可以在失败方法中处理它。

    【讨论】:

    • 我正在使用 Retrofit 2.2。我可以知道如何捕捉 RetrofitException 吗?我有一个被 try catch 包围的拦截器,所以我正在处理 SocketTimeoutException。但问题是如何让我的 CallBack 知道这个特定错误是因为连接超时?
    • @cgr 请看我的回答。
    • @Oclay,感谢您的回复。我终于这样做了。谢谢 ! ;)
    【解决方案5】:

    这有点复杂。使用 Retrofit,您可以进行同步或异步的 API 调用。

    如果您的端点返回 void 并具有回调,则它是异步的。 如果它返回一些东西并且没有回调它是同步的。

    对于异步调用,您会在回调的 onFailure(...) 方法中获得此异常。

    对于同步调用,您根本不会得到它,除非您将调用包装在 try/catch 中。

    try {
       // your synchronous call goes here  
    } catch (RetrofitError error) {
       // handle errors
    }
    

    更新:以上答案适用于 Retrofit 1.9。 Retrofit 2.0 改变了很多。如果您想知道 Retrofit 2.0 现在的工作方式,本文提供了一些建议http://inthecheesefactory.com/blog/retrofit-2.0/en

    【讨论】:

    • 在 Retrofit 2.0 中,没有RetrofitError
    • 谢谢,我更新了我的答案,说它适用于 1.9,并添加了一个链接到一篇关于 2.0 如何工作的文章
    【解决方案6】:

    我发布这个有两个原因:

    1. 我个人尝试增加连接超时,但最终,它并不能真正解决问题的根源。此外,根据this post,用户等待的时间不应超过 10 秒。
    2. 在实际应用中,我们宁愿尽最大努力以尽可能干净的方式实施解决方案。

    所以,这是我在 Kotlin 中提出的解决方案。它的灵感来自@Olcay Ertaş 提供的the answer,并结合Google's recommended architecture 用于Android 应用程序。

    1. 创建TimeoutInterceptor接口:

       interface TimeoutInterceptor : Interceptor
      
    2. 实现TimeoutInterceptor接口:

       class TimeoutInterceptorImpl : TimeoutInterceptor {
      
           override fun intercept(chain: Interceptor.Chain): Response {
               if (isConnectionTimedOut(chain))
                   throw SocketTimeoutException()
               return chain.proceed(chain.request())
           }
      
           private fun isConnectionTimedOut(chain: Interceptor.Chain): Boolean {
               try {
                   val response = chain.proceed(chain.request())
                   val content = response.toString()
                   response.close()
                   Log.d(tag, "isConnectionTimedOut() => $content")
               } catch (e: SocketTimeoutException) {
                   return true
               }
               return false
           }
       }
      
    3. 在您的ApiService 界面中,将TimeoutInterceptor 添加到OkHttpClient 构建器:

       val okHttpClient = OkHttpClient.Builder()
               .addInterceptor(requestInterceptor)
               // Add timeout interceptor
               .addInterceptor(timeoutInterceptor)
               // Set a 5s custom connect timout
               .connectTimeout(5, TimeUnit.SECONDS)
               .build()
      

    您可能已经注意到,您可以设置自定义连接超时。否则,根据documentation,将其保留为 10 秒作为默认值。

    1. 创建一个枚举类ConnectionState。它将提供一个枚举常量对象CONNECTION_TIMEOUT,该对象将进一步用于将适当的连接(或API 调用)状态从EntityNetworkDataSource 类传递到View 类(如果您关注Google's MVVM pattern):

       enum class ConnectionState {
           CONNECTED, NOT_CONNECTED, CONNECTION_TIMEOUT
       }
      
    2. 假设您的EntityNetworkDataSource 界面如下所示:

       interface EntityNetworkDataSource {
           val fetchedEntity: LiveData<Entity>
      
           // Wrap your ConnectionState object in LiveData in order to be able to observe it in the View
           val connectionState: LiveData<ConnectionState>
      
           // Fetch `Entity` object from the network
           suspend fun fetchEntity(id: Int)
       }
      
    3. EntityNetworkDataSource实现类中,可以正确捕获SocketTimeoutException,如下图,在fetchEntity(id: Int)实现内部:

       class EntityNetworkDataSourceImpl(
               private val apiService: ApiService
       ) : EntityNetworkDataSource {
      
           private val _fetchedEntity = MutableLiveData<Entity>()
      
           override val fetchedEntity: LiveData<Entity>
               get() = _fetchedEntity
      
           // We want to keep our MutableLiveData private because they can be changed.
           // So we want to be able to update them only from the inside of this class
           private val _connectionState = MutableLiveData<ConnectionState>()
      
           override val connectionState: LiveData<ConnectionState>
               get() = _connectionState
      
           override suspend fun fetchEntity(id: Int) {
               try {
                   val fetchedEntity = apiService
                           .getEntity(id)
                           .await()
      
                   // Convey the updated connection state to the observer View
                   _connectionState.postValue(ConnectionState.CONNECTED)
      
                   _fetchedEntity.postValue(fetchedEntity)
               } catch (e: SocketTimeoutException) {
                   Log.e(tag, "Connection timeout. ", e)
                   // Catch the SocketTimeoutException and post the updated connection state to the observer View
                   _connectionState.postValue(ConnectionState.CONNECTION_TIMEOUT)
               }
           }
       }
      

    同样的原则适用于您想要拦截和捕获的任何连接异常。

    【讨论】:

    • 大量代码,唯一重要的部分是 fetchEntity() 中的 try..catch ... :(
    【解决方案7】:

    没有一个答案对我很有效,但它们引导我朝着正确的方向前进。看看我在 Kotlin 中是如何做到的。

    您可以在 ErrorInterceptor 中抛出异常并在您的 api 调用函数中捕获它们:

    class ErrorInterceptor : Interceptor {
    
        override fun intercept(chain: Chain): Response {
    
            val request = chain.request()
    
            try {
                val response = chain.proceed(request)
                val bodyString = response.body!!.string()
    
                return response.newBuilder()
                    .body(bodyString.toResponseBody(response.body?.contentType()))
                    .build()
            } catch (e: Exception) {
                when (e) {
                    is SocketTimeoutException -> {
                        throw SocketTimeoutException()
                    }
    
                   // Add additional errors... //
    
                }
            }
        }
    

    或将异常与响应对象捆绑在一起;像这样:

    class ErrorInterceptor : Interceptor {
    
        override fun intercept(chain: Chain): Response {
    
            val request = chain.request()
    
            try {
                val response = chain.proceed(request)
                val bodyString = response.body!!.string()
    
                return response.newBuilder()
                    .body(bodyString.toResponseBody(response.body?.contentType()))
                    .build()
            } catch (e: Exception) {
                var msg = ""
                val interceptorCode: Int
    
                when (e) {
                    is SocketTimeoutException -> {
    
                        msg = "Socket timeout error"
                        interceptorCode = 408
    
                    }
    
                   // Add additional errors... //
    
                }
    
                 return Response.Builder()
                    .request(request)
                    .protocol(Protocol.HTTP_1_1)
                    .code(interceptorCode)
                    .message(msg)
                    .body("{${e}}".toResponseBody(null)).build()
            }
        }
    }
    

    ErrorInterceptor 添加到您的okHttpClient

    okHttpClient.newBuilder()
                    .addInterceptor(ErrorInterceptor())
                    .connectTimeout(10, TimeUnit.SECONDS)
                     // ... //
                    .build()
    

    然后在您的存储库层中类似这样:

    suspend fun makeAPIRequest(): Resource<ApiResponse> {
    
            return withContext(ioDispatcher) {
    
                var response: Response<ApiResponse>? = null
    
                try {
                    response = getResponse()
    
                    // Do additional ops on response here //
    
                } catch (e: Exception) {
    
                    // Exceptions thrown in ErrorInterceptor will propagate here
    
                }
            }
        }
    

    【讨论】:

    • 嘿,这对你有用吗?我试过了,但仍有 50% 的时间异常被拦截器跳过,并且在多个异步 API 调用的情况下没有被捕获。
    • @Arpit J. 拦截器在没有捕获错误时是否还在处理链?我没有将这个错误拦截器用于多个异步调用,但我会在那里开始调查。如果它不处理链,那么它与OkHttp拦截器的工作方式有关,否则它与我的实现有关。如果您设法解决了您的问题,我很想知道出了什么问题以及您是如何解决的。谢谢,祝你好运!
    • 对不起,我已经被淹没了。如果这是一个拦截器问题,我不知道如何解决。你应该看看这样的东西:github.com/square/okhttp/issues/3714
    【解决方案8】:

    科特林

    如果您想在Kotlin 中使用Retrofit,请按照以下步骤操作:

    定义你的改造界面:

    interface GitHubApi {
    
        @GET("/users/{userName}/repos")
        fun repos(@Path("userName") userName: String): Call<List<Repo>>
    }
    

    实施您的服务:

    class Api(...) {
    
        private val baseUrl = "https://api.github.com"
        private val api: GitHubApi
    
        private fun loggingInterceptor(...): HttpLoggingInterceptor {...}
    
        private fun okHttpBuilder(): OkHttpClient {...}
    
        init {...}
    
        fun repos(
            userName: String,
            onSuccess: (list: List<Repo>?) -> Unit,
            onFailure: (message: String?) -> Unit): Future<Unit> {
            return runAsync(api.repos(userName), onSuccess, onFailure)
        }
    
        private fun <T> runAsync(
            call: retrofit2.Call<T>,
            onSuccess: (T?) -> Unit,
            onFailure: (message: String?) -> Unit) : Future<Unit> {
            return doAsync {
                try {
                    val response = call.execute()
                    when {
                        response.isSuccessful -> response.body()?.let {
                            onSuccess(it)
                        }
                        else -> {
                            onFailure(response.raw().message())
                        }
                    }
                } catch (e: IOException) {
                    if (e is SocketTimeoutException) {
                        onFailure("Response time out!")
                    } else {
                        onFailure(e.message)
                    }
                }
            }
        }
    }
    

    在你想要的地方调用你的服务:

    Api().repos("olcayertas",
        onSuccess = {
            Log.d("MainActivity", "Response:\n" + toJson(it))
        },
        onFailure = {
            Log.e("MainActivity", "Error: $it")
        })
    

    您可以在runAsync 函数中处理任何您想要的异常。

    您可以获得完整的工作示例here

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-10
      • 2020-04-20
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      相关资源
      最近更新 更多