web-dev-qa-db-fra.com

Comment réessayer les requêtes HTTP avec OkHttp/Retrofit?

J'utilise Retrofit/OkHttp (1.6) dans mon projet Android. 

Je ne trouve aucun mécanisme de nouvelle tentative de demande intégré à l'un d'eux. En cherchant plus, j'ai lu OkHttp semble avoir des tentatives silencieuses. Je ne vois pas cela se produire sur aucune de mes connexions (HTTP ou HTTPS). Comment configurer les tentatives avec okclient? 

Pour l'instant, j'attrape les exceptions et réessaye en maintenant une variable de compteur.

51
Jaguar

Pour la mise à niveau 1.x;

Vous pouvez utiliser Interceptors . Créer un intercepteur personnalisé 

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

Et utilisez-le en créant RestAdapter.

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

Pour la modification 2.x;

Vous pouvez utiliser Call.clone () method pour cloner une requête et l'exécuter.

65
Sinan Kozak

Je ne sais pas si c'est une option pour vous mais vous pouvez utiliser RxJava avec Retrofit.

Retrofit est capable de renvoyer des observables sur des appels de repos. Sur Oberservables, vous pouvez simplement appeler retry(count) pour vous réabonner à l’observable lorsqu’il émet une erreur.

Vous devez définir l'appel dans votre interface comme suit:

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

Ensuite, vous pouvez vous abonner à cet observable comme ceci:

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

J'ai eu le même problème que toi et c'était en fait ma solution. RxJava est une bibliothèque vraiment agréable à utiliser en combinaison avec Retrofit. Vous pouvez même faire beaucoup de choses intéressantes en plus de réessayer (comme par exemple composer et chaîner des appels ).

34
Jonas Lüthke

Le problème avec response.isSuccessful () se produit lorsque vous avez une exception comme SocketTimeoutException.

J'ai modifié le code d'origine pour le corriger.

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

J'espère que ça aide. Cordialement.

10
Santiago SR

Courtoisie à la réponse du haut, c'est ce qui a fonctionné pour moi. S'il y a des problèmes de connectivité, il est préférable d'attendre quelques secondes avant de réessayer.

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

}

2
Irshu

J'ai trouvé que le chemin (intercepteur OKHttpClient) fourni par Sinan Kozak ne fonctionnait pas lorsque la connexion http échouait, rien ne concernait encore la réponse HTTP. 

Donc, j'utilise un autre moyen pour accrocher l'objet Observable, appelez .retryWhen dessus . Aussi, j'ai ajouté retryCount limit.

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;

Ensuite

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

Ensuite ajouter ou remplacer

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

avec

.addCallAdapterFactory(newCallAdaptorFactory)

Par exemple:

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

Remarque: pour des raisons de simplicité, je traite simplement le code HTTP> 404 comme une nouvelle tentative, veuillez le modifier vous-même.

De plus, si la réponse http est 200, rx.retryWhen ne sera pas appelé si vous souhaitez vérifier une telle réponse, vous pouvez ajouter rx.subscribeOn(...throw error... avant .retryWhen.

1
osexp2003

Je suis d’avis que vous ne devriez pas mélanger la manipulation des API (effectuée par retrofit/okhttp) avec les tentatives. Les mécanismes de relance sont plus orthogonaux et peuvent également être utilisés dans de nombreux autres contextes. J'utilise donc Retrofit/OkHTTP pour tous les appels d'API et le traitement des demandes/réponses, et j'introduis une autre couche au-dessus, pour réessayer l'appel d'API. 

Dans mon expérience limitée de Java jusqu'à présent, j'ai trouvé que la bibliothèque Failsafe de jhlaterman (github: jhalterman/Failafe ) était une bibliothèque très polyvalente pour gérer proprement de nombreuses situations de "tentatives". À titre d’exemple, voici comment je l’utiliserais avec un rétrofit instancié mySimpleService, pour l’authentification - 

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

Le code ci-dessus intercepte les exceptions de socket, les erreurs de connexion, les échecs d'assertion et les tentatives au maximum 3 fois, avec un retour exponentiel. Il vous permet également de personnaliser le comportement lors de nouvelles tentatives et vous permet également de spécifier un repli. Il est tout à fait configurable et peut s’adapter à la plupart des situations de tentative.

N'hésitez pas à consulter la documentation de la bibliothèque car elle propose de nombreux autres avantages, à part de nouvelles tentatives. 

1
Shreyas

Pour ceux qui préfèrent un intercepteur pour traiter la question des nouvelles tentatives - En s'inspirant de la réponse de Sinan, voici le projet d'intercepteur que je propose, qui inclut à la fois le nombre de tentatives et le délai de désactivation, et ne tente que les tentatives lorsque le réseau est disponible n'a pas été annulé . (traite uniquement des exceptions IO (SocketTimeout, UnknownHost, etc.))

    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
sahar

Je veux juste partager ma version. Il utilise la méthode retryWhen de rxJava. Ma version tente à nouveau la connexion toutes les N = 15 secondes et émet presque immédiatement une nouvelle tentative lorsque la connexion Internet est rétablie.

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

Et vous devriez l'utiliser avant .subscribe comme ceci:

.retryWhen(new RetryWithDelayOrInternet())

Vous devez modifier manuellement le champ 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
Nokuap

Une solution qui a fonctionné pour moi sur OkHttp 3.9.1 (en considérant d’autres réponses à cette question):

@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;
}
0
Yamashiro Rion

Il semble qu'il sera présent dans la version 2.0 de la spécification API: https://github.com/square/retrofit/issues/297 . À l'heure actuelle, la meilleure solution semble être une exception à la capture. et réessayez manuellement.

0
Shiva

J'ai beaucoup joué avec ce problème en essayant de trouver le meilleur moyen de réessayer les demandes de modification. J'utilise Retrofit 2 et ma solution est donc pour Retrofit 2. Pour Retrofit 1, vous devez utiliser Interceptor comme la réponse acceptée ici. La réponse de @joluet est correcte mais il n'a pas mentionné le fait que la méthode de nouvelle tentative doit être appelée avant la méthode .subscribe (onComplete, onError). C'est très important sinon la requête ne serait pas rejouée comme @pocmo mentionné dans la réponse @joluet. Voici mon exemple:

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 est mon objet RetrofitServiceProvider.

BTW: J'utilise Java 8, donc beaucoup d'expressions lambda sont dans le code.

0
Sniper

Comme indiqué dans docs , il serait préférable d’utiliser les authentificateurs cuits au four, par exemple: Client final privé OkHttpClient = 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
AllDayAmazing