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


74

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

我找不到任何内置于其中的请求重试机制。在搜索更多内容时,我读到OkHttp似乎有静默重试。我没有在任何连接(HTTP或HTTPS)上看到这种情况。如何使用okclient配置重试?

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


1
@JesseWilson:我发现重试对于速度较慢的网络比更长的连接超时更有用。你认为不是吗?
美洲虎2014年

6
有时api将具有一个响应代码,该响应代码将指示需要进行另一个请求(以重新启动身份验证令牌,会话令牌或XYZ令牌),然后重试原始请求。这在Volley中很容易实现。我想改用改造产品,但是我没有找到一种以通用方式完成此类管道的方法。
danb 2014年

您是否发现了一个比捕获响应异常更好的方法@SlowAndSteady?我目前正在大规模实施此功能,并认为我的类似方法应进行重构。谢谢。
约书亚·品特

@JoshPinter:对不起,找不到其他东西。我不确定OhHttp 2.0是否添加了对此的支持-您可能想看看。
美洲虎2014年

@SlowAndSteady好的,很好,感谢您的更新。作为记录,我决定使用类似于此处概述的模式:stackoverflow.com/a/8658067/293280
Joshua Pinter 2014年

Answers:


83

对于翻新版2.x;

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

对于翻新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 = 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);

连接超时时,响应为空。这样,我们如何检查response.isSuccessful()?
cgr 2015年

2
但是有没有办法计算重试次数?克隆只是复制该调用,以便可以再次执行该调用,但不计入。
Tobias Reich

1
如果没有Internet连接,则该行之后的所有代码Response response = chain.proceed(request);都将无法访问,因为Response将不会收到任何代码。
Yamashiro Rion

5
您需要response.close()在致电重试之前做
列宁·拉杰·拉贾斯卡兰(Linin Raj Rajasekaran)

2
@malhobayyebresponse = chain.call().clone().execute();
桐城

41

我不知道这是否适合您,但是您可以将RxJava与Retrofit一起使用。

改造能够在休息呼叫时返回Observable。在Oberservables上,您可以在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结合使用。除了重试之外,您甚至可以做很多很酷的事情(例如,编写和链接调用)。


11
你曾经尝试过吗?似乎在Retrofit Observable上调用retry()(或只是再次订阅)实际上并没有再次执行该请求。
pocmo

@pocmo请看我的回应,也许会有所帮助
Stoycho Andreev

14

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

到目前为止,以我有限的Java经验,我发现jhlaterman的Failsafe库(github:jhalterman / failsafe)是一个非常通用的库,可以干净地处理许多“重试”情况。举例来说,以下是我如何将其与经过实例化的mySimpleService进行改造的示例,以进行身份​​验证-

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次重试,并以指数补偿。它还允许您自定义重试行为,还可以指定回退。它是可配置的,并且可以适应大多数重试情况。

可以随意检查该库的文档,因为它提供了许多其他功能(除了重试之外)。


5
从设计角度出发具有指导性,指出API调用+处理与重试调用的正交性-一个位于另一个级别上。
非续集,

1
这非常酷,但是2.3.1由于使用的时间单位(ChronoUnit),当前版本似乎需要API 26-
相信

11

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

希望能帮助到你。问候。


但是,即使服务器关闭或任何其他情况,此操作也会重试
Killer'Jul

2
Internet不可用时崩溃。我正在尝试通过SSL请求,添加了自定义标头并添加了另一个日志记录拦截器。
真棒

3
如果我们有超时或没有连接,则返回null,然后生成NullPointerException
Yevhen

OkHttpClient没有setConnectTimeout方法,并且interceptors()是不可变的。答案是否可能基于旧版本?我正在使用<dependency> <groupId> com.squareup.okhttp3 </ groupId> <artifactId> okhttp </ artifactId> <version> 3.11.0 </ version> </ dependency>
Tilman Hausherr

1
要解决的NullPointerException(在没有互联网连接的人如上所述)需要更换return response;,在与终端return response != null ? response : chain.proceed(request);
oxied

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

对于那些更喜欢拦截器来处理重试问题的人-根据思南的回答,这是我建议的拦截器,其中包括重试计数和退避延迟,并且仅在网络可用且请求不存在时重试尝试取消。(仅处理IOExceptions(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;
        }
    });

1

我发现当HTTP连接失败时,Sinan Kozak提供的方式(OKHttpClient拦截器)不起作用,目前还不关心HTTP响应。

所以我用另一种方式钩住Observable对象,在其上调用.retryWhen。另外,我添加了retryCount限制。

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内容不会被调用,如果您坚持检查这样的响应,则可以rx.subscribeOn(...throw error...在.retryWhen之前添加。



0

文档中所述,更好的方法可能是使用身份验证器中的烘焙方法,例如:private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    client.setAuthenticator(new Authenticator() {
      @Override public Request authenticate(Proxy proxy, Response response) {
        System.out.println("Authenticating for response: " + response);
        System.out.println("Challenges: " + response.challenges());
        String credential = Credentials.basic("jesse", "password1");
        return response.request().newBuilder()
            .header("Authorization", credential)
            .build();
      }

      @Override public Request authenticateProxy(Proxy proxy, Response response) {
        return null; // Null indicates no attempt to authenticate.
      }
    });

    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

0

我在解决这个问题上扮演了很多角色,试图找到重试翻新请求的最佳方法。我正在使用Retrofit 2,所以我的解决方案是针对Retrofit2。对于Retrofit 1,您必须像此处接受的答案一样使用Interceptor。@joluet的答案是正确的,但他没有提到需要在.subscribe(onComplete,onError)方法之前调用重试方法。这非常重要,否则将不会像@joluet答案中提到的@pocmo一样重试该请求。这是我的示例:

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表达式。


我已经做到了,并且根据我的日志,retrofitl仅呼叫网络一次。这种方法有问题!
Mohsen Mirhoseini '16

0

只想分享我的版本。它使用rxJava retryWhen方法。我的版本每N = 15秒重试一次连接,并且当Internet连接恢复时几乎立即发出重试。

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

0

工作产品解决方案。

public int callAPI() {
    return 1; //some method to be retried
}

public int retrylogic()  throws InterruptedException, IOException{
    int retry = 0;
    int status = -1;
    boolean delay = false;
    do {
        if (delay) {
            Thread.sleep(2000);
        }

        try {
            status = callAPI();
        }
        catch (Exception e) {
            System.out.println("Error occured");
            status = -1;
        }
        finally {
            switch (status) {
            case 200:
                System.out.println(" **OK**");
                return status; 
            default:
                System.out.println(" **unknown response code**.");
                break;
            }
            retry++;
            System.out.println("Failed retry " + retry + "/" + 3);
            delay = true;

        } 
    }while (retry < 3);

    System.out.println("Aborting download of dataset.");
    return status;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.