I\'m using Retrofit 2 in my project with Observable interface and the Result wrapper. Example:
@POST(\"api/login\")
Observable
You should check if the Throwable thrown is an instance of HttpException.
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.