Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrofit 2 and RxJava error handling operators

I'm using Retrofit 2 in my project with Observable interface and the Result wrapper. Example:

@POST("api/login")
Observable<Result<LoginResponse>> login(@Body LoginRequest request);

I need the Result wrapper to obtain more information from the response than just the serialized object (for example headers, http status...).

Problem is, with the Result wrapper no exception is thrown by the network call. You can find the exception inside the result by calling Result.error().

What should I do if I want to take advantage of RxJava error operators? For example, I'd like to use the retry operator on a network error, but the retry operator only works if an exception is thrown by the observable.

like image 315
Ena Avatar asked Nov 16 '15 16:11

Ena


People also ask

What is the difference between RxJava and retrofit?

Rx gives you a very granular control over which threads will be used to perform work in various points within a stream. To point the contrast here already, basic call approach used in Retrofit is only scheduling work on its worker threads and forwarding the result back into the calling thread.

What happens when an error occurs using RxJava?

RxJava Error Handling That means that after error happened stream is basically finished and no more events can come through it. If Consumer didn't handle error in Observer callback, then that error is sent to a global error handler (which in case of Android crashes the app by default).


1 Answers

Here is the solution I came up with. If I will improve it I will post the changes here.

The solution to my problem (exception swallowed by Retrofit and not handled by RxJava) is the Observable.error method that creates a new observable that only emits the error, so I can "rethrow" the exception.

I created an observable transformer to append to every rest call that emits a retrofit.Result. This transformer takes an Observable> and, if the response has no errors, transforms it into an Observable>. If there are errors it returns an Observable.error with custom Http*Exceptions that I can later handle in my Observer in the onError callback. I put it as a static method of an utility class called ObservableTransformations.resultToResponseWithHttpErrorHandling.

Here it is:

public class ObservableTransformations {

public static <T> Observable.Transformer<Result<T>, Response<T>> resultToResponseWithHttpErrorHandling() {
    return observable -> observable.flatMap(r -> {
        Observable<Response<T>> returnObservable = Observable.just(r.response());
        if (r.isError()) {
            Throwable throwable = r.error();
            if (throwable instanceof IOException) {
                Timber.e(throwable, "Retrofit connection error.");
                // TODO Check this cases
                if (throwable instanceof java.net.ConnectException) {
                    returnObservable = Observable.error(new HttpNoInternetConnectionException());
                } else if (throwable instanceof SocketTimeoutException) {
                    returnObservable = Observable.error(new HttpServerDownException());
                } else {
                    returnObservable = Observable.error(new HttpNoInternetConnectionException());
                }
            } else {
                Timber.e(throwable, "Retrofit general error - fatal.");
                returnObservable = Observable.error(new HttpGeneralErrorException(r.error()));
            }
        } else {
            Response<T> retrofitResponse = r.response();
            if (!retrofitResponse.isSuccess()) {
                int code = retrofitResponse.code();
                String message = "";
                try {
                    message = retrofitResponse.errorBody().string();
                } catch (IOException e) {
                    Timber.e(e, "Error reading errorBody from response");
                }
                Timber.i("Server responded with error. Code: " + code + " message: " + message);
                Throwable t = null;
                if (NetworkUtils.isClientError(code)) {
                    t = new HttpClientException(retrofitResponse.code(), message);
                } else if (NetworkUtils.isServerError(code)) {
                    t = new HttpServerErrorException(retrofitResponse.code(), message);
                }
                returnObservable = Observable.error(t);
            }
        }
        return returnObservable;
    }).retryWhen(new RetryWithDelayIf(3, 1000, t -> {
        return (t instanceof HttpNoInternetConnectionException) || (t instanceof HttpServerDownException);
    }));
}

}

The retry is made 3 times using an exponential backoff, and only if the exception is HttpNoInternetConnectionException or HttpServerDownException.

The RetryWithDelayIf class is here. It takes the condition to be met for retry as the last argument of the constructor (a function taking a throwable and returning true if this throwable should trigger the retry and false if not).

public class RetryWithDelayIf implements
    Func1<Observable<? extends Throwable>, Observable<?>> {

private final int maxRetries;
private final int retryDelayMillis;
private int retryCount;
private Func1<Throwable, Boolean> retryIf;

public RetryWithDelayIf(final int maxRetries, final int retryDelayMillis, Func1<Throwable, Boolean> retryIf) {
    this.maxRetries = maxRetries;
    this.retryDelayMillis = retryDelayMillis;
    this.retryCount = 0;
    this.retryIf = retryIf;
}

@Override
public Observable<?> call(Observable<? extends Throwable> attempts) {
    return attempts.zipWith(Observable.range(1, maxRetries + 1), (n, i) -> {
        return new Tuple<Throwable, Integer>(n, i);
    })
            .flatMap(
                    ni -> {
                        if (retryIf.call(ni.getFirst()) && ni.getSecond() <= maxRetries) {
                            return Observable.timer((long) Math.pow(2, ni.getSecond()), TimeUnit.SECONDS);
                        } else {
                            return Observable.error(ni.getFirst());
                        }
                    });
}

}

Finally, here is the usage with a restService call:

restService.login(new LoginRestRequest(username, password))
                .compose(ObservableTransformations.resultToResponseWithHttpErrorHandling());

In the onError of your observer you can finally handle the Http*Exceptions.

like image 120
Ena Avatar answered Sep 30 '22 18:09

Ena