如何使用OkHttp/Retrofit重试HTTP请求?

96

我正在我的Android项目中使用Retrofit/OkHttp(1.6)。

我没有发现它们中的任何一个内置请求重试机制。在进一步搜索后,我了解到OkHttp似乎具有无声重试功能。但我在我的任何连接(HTTP或HTTPS)上都没有看到这种情况发生。如何使用okclient配置重试?

目前,我正在捕获异常并维护计数器变量进行重试。


1
@JesseWilson:我认为在网络较慢的情况下,重试比延长连接超时时间更有用。你怎么看? - dev
6
有时候API会返回一个响应码,指示需要发出另一个请求(以重新获取授权令牌、会话令牌或XYZ令牌)然后重试原始请求。在Volley中很容易实现这一点。我想改用Retrofit,但是我不知道如何以通用的方式完成这种工作。 - danb
你有没有找到比捕获响应异常更好的方法,@SlowAndSteady?我目前正在大规模实施这个方法,认为我的类似方法需要进行重构。谢谢。 - Joshua Pinter
@JoshPinter:抱歉,没有找到其他的东西。我不确定OhHttp 2.0是否已经添加了对此的支持 - 你可能需要看一下。 - dev
@SlowAndSteady 好的,太棒了,感谢您的更新。为了记录,我决定使用类似于这里概述的模式:https://dev59.com/mGoy5IYBdhLWcg3wQ8AF#8658067 - Joshua Pinter
15个回答

103

对于Retrofit 2.x:

您可以使用 Call.clone()方法来克隆请求并执行它。

对于Retrofit 1.x:

您可以使用拦截器。创建一个自定义的拦截器。

    OkHttpClient client = new OkHttpClient();
    client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.interceptors().add(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            // try the request
            Response response = chain.proceed(request);

            int tryCount = 0;
            while (!response.isSuccessful() && tryCount < 3) {

                Log.d("intercept", "Request is not successful - " + tryCount);

                tryCount++;

                // retry the request
                response.close()
                response = chain.proceed(request);
            }

            // otherwise just pass the original response on
            return response;
        }
    });

并在创建RestAdapter时使用它。

new RestAdapter.Builder()
        .setEndpoint(API_URL)
        .setRequestInterceptor(requestInterceptor)
        .setClient(new OkClient(client))
        .build()
        .create(Adapter.class);

2
但是有没有一种方法来计算重试次数呢?克隆只是复制调用,因此它可以再次执行,但不会计数。 - Tobias Reich
1
当没有网络连接时,位于 Response response = chain.proceed(request); 这行代码之后的所有代码都将无法执行,因为不会收到任何 Response - Yamashiro Rion
9
在重试之前,你需要执行 response.close() - Raj
call.clone 如何使用? - malhobayyeb
2
@malhobayyeb response = chain.call().clone().execute(); @malhobayyeb response = chain.call().clone().execute(); - TongChen
显示剩余4条评论

48

我不知道这对你是否可行,但你可以使用RxJava和Retrofit一起使用。

Retrofit能够返回rest调用时的Observables。在Observables上,您只需调用retry(count)即可在Observable发生错误时重新订阅。

您需要像这样在接口中定义调用:

@GET("/data.json")
Observable<DataResponse> fetchSomeData();

那么你可以像这样订阅这个Observable:

restApi.fetchSomeData()
.retry(5)  // Retry the call 5 times if it errors
.subscribeOn(Schedulers.io())  // execute the call asynchronously
.observeOn(AndroidSchedulers.mainThread())  // handle the results in the ui thread
.subscribe(onComplete, onError); 
// onComplete and onError are of type Action1<DataResponse>, Action1<Throwable>
// Here you can define what to do with the results

我和你有同样的问题,这是我的解决方案。RxJava是与Retrofit结合使用的非常好的库。除了重试之外,您甚至可以做许多很酷的事情(例如 组合和链接调用)。


14
你是否尝试过这样做?调用Retrofit Observable的retry()方法(或者仅仅再次进行订阅)似乎并不能重新发起请求。 - pocmo
@pocmo,请看一下我的回复,也许会有帮助。 - Stoycho Andreev

26

我认为您不应该将API处理(由retrofit / okhttp完成)与重试混合在一起。重试机制更加正交,可以在许多其他情境中使用。因此,我使用Retrofit/OkHTTP处理所有API调用和请求/响应处理,并在其上方引入另一层来重试API调用。

在我有限的Java经验中,我发现jhlaterman的Failsafe库(GitHub:jhalterman/failsafe)非常适用于清晰地处理许多“重试”情况。例如,这是我如何将其与一个已实例化的mySimpleService retrofit一起用于身份验证:

AuthenticationResponse authResp = Failsafe.with(
new RetryPolicy().retryOn(Arrays.asList(IOException.class, AssertionError.class))
        .withBackoff(30, 500, TimeUnit.MILLISECONDS)
        .withMaxRetries(3))
.onRetry((error) -> logger.warn("Retrying after error: " + error.getMessage()))
.get(() -> {
    AuthenticationResponse r = mySimpleAPIService.authenticate(
            new AuthenticationRequest(username,password))
            .execute()
            .body();

    assert r != null;

    return r;
});
上述代码捕获套接字异常、连接错误、断言失败,并最多重试3次,采用指数退避。它还允许您自定义在重试时的行为,并允许您指定备选方案。它非常可配置,并且可以适应大多数重试情况。 随意查阅该库的文档,因为除了重试之外,它还提供许多其他好处。

7
从设计的角度来看,指出API调用和处理与重试调用之间正交的特性是具有教育意义的——其中一个处于比另一个更高的层次。 - non sequitor
1
这很酷,但当前版本 2.3.1 似乎需要 API 26,因为使用了时间单位(ChronoUnit)。 - behelit
@behelit,有什么问题吗?当你写下这条评论时,API 26已经发布了两年多的时间(于2017年8月发布)。我认识的大多数专业Android开发者都通过Google Play分发他们的应用程序。如果您想使用Play,您必须保持SDK / API级别最新...自2018年8月起,Google强制要求您在新应用程序中使用API 26,并自2018年11月起对现有应用程序进行更新。 - The incredible Jan

15

response.isSuccessful()存在的问题在于当您遇到像SocketTimeoutException这样的异常时。

我修改了原始代码以解决此问题。

OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.interceptors().add(new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        boolean responseOK = false;
        int tryCount = 0;

        while (!responseOK && tryCount < 3) {
            try {
                 response = chain.proceed(request);
                 responseOK = response.isSuccessful();                  
            }catch (Exception e){
                 Log.d("intercept", "Request is not successful - " + tryCount);                     
            }finally{
                 tryCount++;      
            }
        }

        // otherwise just pass the original response on
        return response;
    }
});

希望能有所帮助。 敬礼。


但是这将会重试,即使服务器宕机或出现其他情况。 - Shubham AgaRwal
2
当网络不可用时,它会崩溃。我正在尝试通过SSL请求,添加自定义标头并添加另一个日志拦截器。 - Awesome
3
如果超时或没有连接,它会返回null,然后引发NullPointerException异常。 - Yevhen
1
为了解决空指针异常(就像前面提到的没有网络连接一样),需要将最后的return response;替换为return response != null ? response : chain.proceed(request); - oxied
1
@Tilman Hausherr OkHttpClient.Builder().readTimeout(10,TimeUnit.SECONDS) .writeTimeout(10,TimeUnit.SECONDS) .connectTimeout(10,TimeUnit.SECONDS) - The incredible Jan
显示剩余2条评论

4

感谢最佳答案的提供,这是对我本人有用的。如果在连接时出现问题,最好等待几秒钟后再重试。

public class ErrorInterceptor implements Interceptor {
ICacheManager cacheManager;
Response response = null;
int tryCount = 0;
int maxLimit = 3;
int waitThreshold = 5000;
@Inject
public ErrorInterceptor() {

}

@Override
public Response intercept(Chain chain){

   // String language =  cacheManager.readPreference(PreferenceKeys.LANGUAGE_CODE);
  Request request = chain.request();
  response =  sendReqeust(chain,request);
    while (response ==null && tryCount < maxLimit) {
        Log.d("intercept", "Request failed - " + tryCount);
        tryCount++;
        try {
            Thread.sleep(waitThreshold); // force wait the network thread for 5 seconds
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       response = sendReqeust(chain,request);
    }
    return response;
}

private Response sendReqeust(Chain chain, Request request){
    try {
        response = chain.proceed(request);
        if(!response.isSuccessful())
            return null;
        else
        return response;
    } catch (IOException e) {
      return null;
    }
}

}


你能帮我吗?当服务器返回错误500或其他错误时,它无法工作。--> HTTP失败:java.lang.IllegalStateException:无法发出新请求,因为先前的响应仍然打开。 - Ahmed D. Sherif

3

以下是一个对我有效的解决方案,适用于OkHttp 3.9.1(考虑到其他回答此问题的方法):

@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
    Request  request      = chain.request();
    int      retriesCount = 0;
    Response response     = null;

    do {
        try {
            response = chain.proceed(request);

        // Retry if no internet connection.
        } catch (ConnectException e) {
            Log.e(TAG, "intercept: ", e);
            retriesCount++;

            try {
                Thread.sleep(RETRY_TIME);

            } catch (InterruptedException e1) {
                Log.e(TAG, "intercept: ", e1);
            }
        }

    } while (response == null && retriesCount < MAX_RETRIES);

    // If there was no internet connection, then response will be null.
    // Need to initialize response anyway to avoid NullPointerException.
    if (response == null) {
        response = chain.proceed(newRequest);
    }

    return response;
}

2

对于那些更喜欢使用拦截器处理重试问题的人 - 基于Sinan的答案,这是我提出的拦截器,它包括重试计数和退避延迟,并且只在网络可用且请求未被取消时重试尝试。 (仅处理IOException(SocketTimeout,UnknownHost等))

    builder.addInterceptor(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            // try the request
            Response response = null;
            int tryCount = 1;
            while (tryCount <= MAX_TRY_COUNT) {
                try {
                    response = chain.proceed(request);
                    break;
                } catch (Exception e) {
                    if (!NetworkUtils.isNetworkAvailable()) {
                        // if no internet, dont bother retrying request
                        throw e;
                    }
                    if ("Canceled".equalsIgnoreCase(e.getMessage())) {
                        // Request canceled, do not retry
                        throw e;
                    }
                    if (tryCount >= MAX_TRY_COUNT) {
                        // max retry count reached, giving up
                        throw e;
                    }

                    try {
                        // sleep delay * try count (e.g. 1st retry after 3000ms, 2nd after 6000ms, etc.)
                        Thread.sleep(RETRY_BACKOFF_DELAY * tryCount);
                    } catch (InterruptedException e1) {
                        throw new RuntimeException(e1);
                    }
                    tryCount++;
                }
            }

            // otherwise just pass the original response on
            return response;
        }
    });

2
我发现Sinan Kozak提供的方法(OKHttpClient拦截器)在http连接失败时无法工作,与HTTP响应没有关系。
因此,我使用了另一种方法来挂钩Observable对象,在其上调用.retryWhen。此外,我还添加了重试次数限制。
import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.HttpException;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;
import rx.Observable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

那么

    RxJavaCallAdapterFactory originCallAdaptorFactory = RxJavaCallAdapterFactory.create();

    CallAdapter.Factory newCallAdaptorFactory = new CallAdapter.Factory() {
        @Override
        public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {

            CallAdapter<?> ca = originCallAdaptorFactory.get(returnType, annotations, retrofit);

            return new CallAdapter<Observable<?>>() {

                @Override
                public Type responseType() {
                    return ca.responseType();
                }

                int restRetryCount = 3;

                @Override
                public <R> Observable<?> adapt(Call<R> call) {
                    Observable<?> rx = (Observable<?>) ca.adapt(call);

                    return rx.retryWhen(errors -> errors.flatMap(error -> {
                        boolean needRetry = false;
                        if (restRetryCount >= 1) {
                            if (error instanceof IOException) {
                                needRetry = true;
                            } else if (error instanceof HttpException) {
                                if (((HttpException) error).code() != 200) {
                                    needRetry = true;
                                }
                            }
                        }

                        if (needRetry) {
                            restRetryCount--;
                            return Observable.just(null);
                        } else {
                            return Observable.error(error);
                        }
                    }));
                }
            };
        }
    };                

然后添加或替换。
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

使用

.addCallAdapterFactory(newCallAdaptorFactory)

例如:

return new Retrofit
        .Builder()
        .baseUrl(baseUrl)
        .client(okClient)
        .addCallAdapterFactory(newCallAdaptorFactory)
        .addConverterFactory(JacksonConverterFactory.create(objectMapper));

注意:为了简单起见,我将HTTP代码> 404代码视为重试,请根据自己的需要进行修改。
此外,如果HTTP响应是200,则上述rx.retryWhen将不会被调用。如果您坚持检查这样的响应,则可以在.retryWhen之前添加rx.subscribeOn(...抛出错误...

1
我想分享我的版本。它使用了rxJava的retryWhen方法。我的版本每隔N=15秒重试连接,当网络连接恢复时几乎立即发出重试信号。
public class RetryWithDelayOrInternet implements Function<Flowable<? extends Throwable>, Flowable<?>> {
public static boolean isInternetUp;
private int retryCount;

@Override
public Flowable<?> apply(final Flowable<? extends Throwable> attempts) {
    return Flowable.fromPublisher(s -> {
        while (true) {
            retryCount++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                attempts.subscribe(s);
                break;
            }
            if (isInternetUp || retryCount == 15) {
                retryCount = 0;
                s.onNext(new Object());
            }
        }
    })
            .subscribeOn(Schedulers.single());
}}

在使用.subscribe之前,您应该像这样使用它:

.retryWhen(new RetryWithDelayOrInternet())

你应该手动更改isInternetUp字段

public class InternetConnectionReceiver extends BroadcastReceiver {


@Override
public void onReceive(Context context, Intent intent) {
    boolean networkAvailable = isNetworkAvailable(context);
    RetryWithDelayOrInternet.isInternetUp = networkAvailable;
}
public static boolean isNetworkAvailable(Context context) {
    ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
    return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}}

1
我一直在尝试找到最好的重试Retrofit请求的方法,使用的是Retrofit 2,所以我的解决方案适用于Retrofit 2。对于Retrofit 1,您必须像这里接受的答案一样使用Interceptor。@joluet的答案是正确的,但他没有提到需要在.subscribe(onComplete, onError)方法之前调用retry方法。这非常重要,否则请求将不会像@pocmo在@joluet的答案中提到的那样重新尝试。这是我的例子:
final Observable<List<NewsDatum>> newsDetailsObservable = apiService.getCandidateNewsItem(newsId).map((newsDetailsParseObject) -> {
                    return newsDetailsParseObject;
                });

newsDetailsObservable.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .retry((integer, throwable) -> {
                //MAX_NUMBER_TRY is your maximum try number
                if(integer <= MAX_NUMBER_TRY){
                    return true;//this will retry the observable (request)
                }
                return false;//this will not retry and it will go inside onError method
            })
            .subscribe(new Subscriber<List<NewsDatum>>() {
                @Override
                public void onCompleted() {
                    // do nothing
                }

                @Override
                public void onError(Throwable e) {
                   //do something with the error
                }

                @Override
                public void onNext(List<NewsDatum> apiNewsDatum) {
                    //do something with the parsed data
                }
            });

apiService是我的RetrofitServiceProvider对象。

顺便说一下:我正在使用Java 8,因此代码中有很多lambda表达式。


我已经完成了这个任务,根据我的日志,Retrofit只调用了一次网络。这个方法肯定有问题! - Mohsen Mirhoseini

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