Wie versuche ich HTTP-Anfragen mit OkHttp / Retrofit erneut?

74

Ich verwende Retrofit / OkHttp (1.6) in meinem Android-Projekt.

Ich finde in keinem von beiden einen Mechanismus für Wiederholungswiederholungen eingebaut. Bei der Suche nach mehr habe ich gelesen, dass OkHttp stille Wiederholungsversuche zu haben scheint. Ich sehe das bei keiner meiner Verbindungen (HTTP oder HTTPS). Wie konfiguriere ich Wiederholungsversuche mit okclient?

Im Moment fange ich Ausnahmen ab und versuche erneut, eine Zählervariable beizubehalten.

Jaguar
quelle
1
@ JesseWilson: Ich finde das Wiederholen für langsamere Netzwerke nützlicher als längere Verbindungszeitüberschreitungen. Denkst du anders
Jaguar
6
Manchmal hat eine API einen Antwortcode, der angibt, dass eine andere Anforderung gestellt werden muss (um Authentifizierungstoken oder Sitzungstoken oder XYZ-Token erneut zu erstellen), und dann wird die ursprüngliche Anforderung erneut versucht. Dies ist in Volley einfach zu erreichen. Ich würde gerne auf Nachrüstung umsteigen, aber ich sehe keinen Weg, um diese Art von Sanitär auf generische Weise zu erreichen.
Danb
Haben Sie eine bessere Methode gefunden, als nur Antwortausnahmen abzufangen, @SlowAndSteady? Ich implementiere dies derzeit in größerem Maßstab und dachte, meine ähnliche Methode sei für einen Refactor vorgesehen. Vielen Dank.
Joshua Pinter
@JoshPinter: Entschuldigung, ich konnte nichts anderes finden. Ich bin mir nicht sicher, ob OhHttp 2.0 dies unterstützt hat - vielleicht möchten Sie einen Blick darauf werfen.
Jaguar
@SlowAndSteady Okay, großartig, danke für das Update. Für die Aufzeichnung habe ich beschlossen, ein Muster zu verwenden, das dem hier beschriebenen ähnlich ist: stackoverflow.com/a/8658067/293280
Joshua Pinter

Antworten:

81

Zur Nachrüstung 2.x;

Sie können die Call.clone () -Methode verwenden, um die Anforderung zu klonen und auszuführen.

Zur Nachrüstung 1.x;

Sie können Interceptors verwenden . Erstellen Sie einen benutzerdefinierten Interceptor

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

Und verwenden Sie es beim Erstellen von RestAdapter.

new RestAdapter.Builder()
        .setEndpoint(API_URL)
        .setRequestInterceptor(requestInterceptor)
        .setClient(new OkClient(client))
        .build()
        .create(Adapter.class);
Sinan Kozak
quelle
Wenn bei der Verbindung eine Zeitüberschreitung auftritt, ist die Antwort null. Wie können wir damit response.isSuccessful () überprüfen?
cgr
2
Aber gibt es eine Möglichkeit, die Wiederholungsversuche zu zählen? Clone kopiert den Aufruf nur, damit er erneut ausgeführt werden kann, aber er zählt nicht.
Tobias Reich
1
Wenn keine Internetverbindung besteht, ist der gesamte Code nach der Leitung Response response = chain.proceed(request);nicht erreichbar, da kein Code Responseempfangen wird.
Yamashiro Rion
4
Sie müssen tun, response.close()bevor Sie erneut versuchen
Lenin Raj Rajasekaran
2
@malhobayyebresponse = chain.call().clone().execute();
TongChen
41

Ich weiß nicht, ob dies eine Option für Sie ist, aber Sie könnten RxJava zusammen mit Retrofit verwenden.

Retrofit kann Observables bei Ruhegesprächen zurückgeben. Auf Oberservables können Sie einfach anrufen retry(count), um das Observable erneut zu abonnieren, wenn es einen Fehler ausgibt.

Sie müssten den Aufruf in Ihrer Schnittstelle folgendermaßen definieren:

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

Dann können Sie dieses Observable wie folgt abonnieren:

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

Ich hatte das gleiche Problem wie Sie und dies war eigentlich meine Lösung. RxJava ist eine wirklich schöne Bibliothek, die in Kombination mit Retrofit verwendet werden kann. Sie können neben dem erneuten Versuch sogar viele coole Dinge tun (z. B. das Verfassen und Verketten von Anrufen ).

Jonas Lüthke
quelle
11
Hast du das jemals versucht? Es scheint, als würde der Aufruf von retry () (oder das erneute Abonnieren) eines Retrofit Observable die Anforderung nicht erneut ausführen.
Pocmo
@pocmo pls sehen meine Antwort, vielleicht wird es helfen
Stoycho Andreev
14

Ich bin der Meinung, dass Sie das API-Handling (durchgeführt durch retrofit / okhttp) nicht mit Wiederholungsversuchen mischen sollten. Wiederholungsmechanismen sind orthogonaler und können auch in vielen anderen Kontexten verwendet werden. Daher verwende ich Retrofit / OkHTTP für alle API-Aufrufe und die Verarbeitung von Anforderungen / Antworten und füge oben eine weitere Ebene ein, um den API-Aufruf erneut zu versuchen.

In meiner bisher begrenzten Java-Erfahrung habe ich festgestellt, dass die Failsafe-Bibliothek von jhlaterman (github: jhalterman / failafe ) eine sehr vielseitige Bibliothek ist, um viele Wiederholungssituationen sauber zu handhaben. Als Beispiel würde ich Folgendes mit einem nachgerüsteten instanziierten mySimpleService zur Authentifizierung verwenden:

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

Der obige Code fängt Socket-Ausnahmen, Verbindungsfehler, Assertionsfehler ab und versucht sie maximal dreimal mit exponentiellem Backoff. Außerdem können Sie das Verhalten bei erneuten Versuchen anpassen und einen Fallback angeben. Es ist ziemlich konfigurierbar und kann sich an die meisten Wiederholungssituationen anpassen.

Sie können gerne die Dokumentation der Bibliothek überprüfen, da sie neben nur Wiederholungsversuchen noch viele andere Extras bietet.

Shreyas
quelle
5
Aus gestalterischer Sicht lehrreich, um auf die orthogonale Natur der API-Anruf + -Verarbeitung im Vergleich zum erneuten Versuch der Anrufe hinzuweisen - einer befindet sich auf einer höheren Ebene als der andere.
Nicht-Sequitor
1
Dies ist ziemlich cool, aber die aktuelle Version 2.3.1scheint API 26 aufgrund der verwendeten Zeiteinheiten (ChronoUnit) zu erfordern
siehe
11

Das Problem mit response.isSuccessful () besteht darin, dass Sie eine Ausnahme wie SocketTimeoutException haben.

Ich habe den ursprünglichen Code geändert, um ihn zu beheben.

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

Ich hoffe es hilft. Grüße.

Santiago SR
quelle
Aber dies wird erneut versucht, selbst wenn der Server ausfällt oder in einem anderen Fall
Killer
2
Es stürzt ab, wenn das Internet nicht verfügbar ist. Ich versuche eine Anfrage über SSL, füge benutzerdefinierte Header hinzu und füge einen weiteren Protokollierungs-Interceptor hinzu.
Super
3
Wenn wir eine Zeitüberschreitung oder keine Verbindung haben, gibt es null zurück und generiert dann NullPointerException
Yevhen
OkHttpClienthat keine setConnectTimeoutMethode und interceptors()ist unveränderlich. Könnte es sein, dass diese Antwort auf einer alten Version basiert? Ich verwende <dependency> <groupId> com.squareup.okhttp3 </ groupId> <artifactId> okhttp </ifactId> <version> 3.11.0 </ version> </ dependency>
Tilman Hausherr
1
Um nullPointerException zu beheben (wenn keine Internetverbindung besteht, wie oben erwähnt), muss return response;am Ende durchreturn response != null ? response : chain.proceed(request);
oxied
4

Mit freundlicher Genehmigung der Top-Antwort: Dies hat bei mir funktioniert. Wenn es Verbindungsprobleme gibt, sollten Sie einige Sekunden warten, bevor Sie es erneut versuchen.

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

}}

Irshu
quelle
können Sie mir helfen?? Wenn der Server den Fehler 500 oder andere ausgibt, funktioniert er nicht -> HTTP FAILED: java.lang.IllegalStateException: Es kann keine neue Anfrage gestellt werden, da die vorherige Antwort noch offen ist
Ahmed D. Sherif
3

Eine Lösung, die für mich unter OkHttp 3.9.1 funktioniert hat (unter Berücksichtigung anderer Antworten auf diese Frage):

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

Für diejenigen, die einen Interceptor bevorzugen, um sich mit dem Problem des erneuten Versuchens zu befassen - Aufbauend auf Sinans Antwort ist hier mein vorgeschlagener Interceptor, der sowohl die Anzahl der Wiederholungen als auch die Verzögerung des Zurücksetzens umfasst und nur Versuche wiederholt, wenn das Netzwerk verfügbar ist und wenn keine Anforderung vorliegt abgesagt. (behandelt nur IOExceptions (SocketTimeout, UnknownHost usw.))

    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;
        }
    });
Sahar
quelle
1

Ich habe festgestellt, dass der von Sinan Kozak bereitgestellte Weg (OKHttpClient Intercepter) nicht funktioniert, wenn die http-Verbindung fehlschlägt. Es gibt noch nichts, was die HTTP-Antwort betrifft.

Also benutze ich eine andere Möglichkeit, um das Observable-Objekt zu verknüpfen, und rufe .retryWhen auf. Außerdem habe ich retryCount Limit hinzugefügt.

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;

Dann

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

Dann hinzufügen oder ersetzen

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

mit

.addCallAdapterFactory(newCallAdaptorFactory)

Zum Beispiel:

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

Hinweis: Der Einfachheit halber behandle ich HTTP-Code> 404-Code nur als Wiederholungsversuch. Bitte ändern Sie ihn selbst.

Wenn die http-Antwort 200 ist, wird die oben rx.retryWhengenannte Antwort nicht aufgerufen. Wenn Sie darauf bestehen, eine solche Antwort zu überprüfen, können Sie sie rx.subscribeOn(...throw error...vor .retryWhen hinzufügen .

osexp2003
quelle
0

Wie in den Dokumenten angegeben , ist es möglicherweise besser, die eingebrannten Authentifikatoren zu verwenden, z. B.: 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());
  }
AllDayAmazing
quelle
0

Ich habe viel mit diesem Problem gespielt und versucht herauszufinden, wie Retrofit-Anforderungen am besten wiederholt werden können. Ich verwende Retrofit 2, daher ist meine Lösung Retrofit 2. Für Retrofit 1 müssen Sie Interceptor wie die hier akzeptierte Antwort verwenden. Die Antwort von @joluet ist korrekt, aber er hat nicht erwähnt, dass die Wiederholungsmethode vor der .subscribe-Methode (onComplete, onError) aufgerufen werden muss. Dies ist sehr wichtig, da sonst die Anfrage nicht erneut versucht wird, wie in der Antwort von @joluet erwähnt. Hier ist mein Beispiel:

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 ist mein RetrofitServiceProvider-Objekt.

Übrigens: Ich verwende Java 8, daher sind viele Lambda-Ausdrücke im Code enthalten.

Stoycho Andreev
quelle
Ich habe dies getan und laut meinen Protokollen rufe ich das Netzwerk nur einmal nach. Mit dieser Methode stimmt etwas nicht!
Mohsen Mirhoseini
0

Ich möchte nur meine Version teilen. Es verwendet die Methode rxJava retryWhen. Meine Version wiederholt die Verbindung alle N = 15 Sekunden und gibt fast sofort einen erneuten Versuch aus, wenn die Internetverbindung wiederhergestellt ist.

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

Und Sie sollten es verwenden, bevor Sie sich wie folgt anmelden:

.retryWhen(new RetryWithDelayOrInternet())

Sie sollten das Feld isInternetUp manuell ändern

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

Arbeitsproduktlösung.

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