Forcing request retry after custom API exceptions in RxJava - android

I'm really new in RxJava, but I'm trying to implement an API using Retrofit framework and RxJava.
On a server side there is an authorization service which handles user's session and in case of some delay in user's actions server breaks his session. After that user has to login again in order to perform new API call. Bad thing is - server always returns HTTP code 200 and for notification about expiration uses some custom JSON response with expiration code, so RxJava doesn't fire Exception during onNext operation because RxJava considers that request was passed successfully.
And the question is: How to implement correct flow to handle custom API exceptions like expiration and retry failed request after some other request (in my case relogin)?
Something like this:
app -> login()
server -> { code:0, ... }
app -> getUsers()
server -> { code:0, ... }
------- in 30 minutes -------
app -> getPicture()
server -> { code:99, ... } // session expired, user unauthorized
app -> login()
server -> { code:0, ... }
app -> getPicture()
server -> { code:0, ... }
I was thinking about something like this, but with no success:
Observable.create(new Observable.OnSubscribe<BackendResponse<String>>() {
#Override
public void call(Subscriber<? super Response<String>> subscriber) {
try {
Response<String> response;
subscriber.onNext(response = getInterface().getUsers());
if (response != null) {
response.checkData(); // throws ServerException in case of code != 0
}
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
}
}).subscribeOn(Schedulers.io()).retryWhen(new RetryWithSessionRefresh(new SessionService())).subscribe();
and RetryWithSessionRefresh is:
public class RetryWithSessionRefresh implements
Func1<Observable<? extends Notification<?>>, Observable<?>> {
private final SessionService sessionSerivce;
public RetryWithSessionRefresh(SessionService sessionSerivce) {
this.sessionSerivce = sessionSerivce;
}
#Override
public Observable<?> call(Observable<? extends Notification<?>> attempts) {
return attempts
.flatMap(new Func1<Notification<?>, Observable<?>>() {
#Override
public Observable<?> call(final Notification notification) {
final Throwable throwable = notification.getThrowable();
if (throwable instanceof ServerException) {
final ServerException backendException = (ServerException) throwable;
if (backendException.getBackendErrorCode() == Response.AUTHORIZATION_FAILED) {
return sessionSerivce
.observeSessionToken()
.doOnNext(new Action1<TokenCallback>() {
#Override
public void call(TokenCallback token) {
if (token != null) {
DataHolder.getInstance().setAuthToken(token.getToken());
}
}
})
.doOnError(new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
DataHolder.getInstance().setAuthToken("");
}
});
}
}
return Observable.error(notification.getThrowable());
}
});
}

Maybe you van flapMap your response, and return your observable in case of success with Observable.just(your item) or an error when the response isn't valid with Observable.error(your error)

Related

retrofit2 rxjava 2 - how can access to body of response when have error

I use Retrofit with Rxjava together for request to server.
the my server return defined json format that Include data , and defined message.
server return http response. it's ok if server return success code(200).
but I want, if server return other code, i manage the body of that response.
for example:
the server return 401, and I want read body of response for show message of the server.
but when server other code, retrofit call the onError method and I can't use body of response.
how can solve this problem?
this is my mehod
'''
private void login(String username , String password){
view.setLoading();
source.loginUser(username, password)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Response<LoginResult>>() {
#Override
public void onSubscribe(Disposable d) {
disposable.add(d);
}
#Override
public void onSuccess(Response<LoginResult> loginResult) {
if (loginResult.isSuccessful()){
}
else
new AlertConfiguration(view.getViewActivity()).showMessage(loginResult.body().getMessage());
}
#Override
public void onError(Throwable e) {
if there is a problem
}
});
'''
and this is my interface method for retrofit
#POST("...")
Single<Response<LoginResult>> loginUser(#Query("username") String username, #Query("password") String password);
According to info here, since you are already using Response<LoginResult> you should be able to cast the Throwable to a HttpException, and then extract the body of the response.
In Kotlin:
if (e is HttpException) {
val errorBody = (e as HttpException).response().errorBody()
}
In Java:
if (e instanceof HttpException) {
HttpException error = (HttpException)e;
String errorBody = error.response().errorBody().string();
}
Complete example using your Java code:
import retrofit2.HttpException
private void login(String username , String password){
view.setLoading();
source.loginUser(username, password)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Response<LoginResult>>() {
#Override
public void onSubscribe(Disposable d) {
disposable.add(d);
}
#Override
public void onSuccess(Response<LoginResult> loginResult) {
if (loginResult.isSuccessful()){
// Handle success
} else {
// Handle login failure
new AlertConfiguration(view.getViewActivity()).showMessage(loginResult.body().getMessage());
}
}
#Override
public void onError(Throwable e) {
// Get the error response:
if (e instanceof HttpException) {
HttpException error = (HttpException)e;
String errorBody = error.response().errorBody().string();
// Then parse the errorBody and extract the values you need
}
}
});
I find solution
in retrofit onSuccess methode when response code is 200, you should get response from body object.
but when isn't 200, you should get response from errorBody;
new Gson().fromJson(serverResultResponse.errorBody().string(), ServerResult.class);

Retrofit 2 and rxJava with Flowable concat handle several response

I'm using retrofit, rxJava and realm I'm using flowable concat for launch a request to the server and another one for realm
This is how I'm creating the disposable :
disposable.add(launchRequest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Config>() {
#Override
public void accept(Config config) throws Exception {
ProfileManager.getInstance().setConfig(config);
}
}, new Consumer<Throwable>() {
#Override
public void accept(#NonNull Throwable throwable) throws Exception {
if (!throwable.getMessage().equals(ErrorUtils.ERROR_CODE_304)) {
throwable.printStackTrace();
}
}
}));
launchRequest function create the Flowable :
Flowable.concat(cloudDataStore.getById(single, diskDataStore, repositoryItem).toFlowable(), diskDataStore.getById(id, repositoryItem).toFlowable())
.map(new Function<Object, T>() {
#Override
public T apply(Object o) throws Exception {
return repositoryItem.toView((T) o);
}
});
cloudDataStore param is the retrofit part and diskDataStore is the realm part.
Everything workings fine here, my trouble is when I handle the retrofit request :
return single.flatMap(new Function<Response<T1>, SingleSource<T1>>() {
#Override
public SingleSource<T1> apply(#NonNull Response<T1> response) throws Exception {
if (response.code() == Integer.valueOf(ErrorUtils.CODE_200)) {
return Single.just(response.body());
} else if (response.code() == Integer.valueOf(ErrorUtils.ERROR_CODE_304)) {
return Single.just(response.body());
} else {
return Single.error(new Throwable(String.valueOf(response.code())));
}
}
});
If the request is sucessfull (status 200) I return the server response to my disposable.
If I got a code 304 the response body is null so the disposable throwable is trigger, but if the throwable is trigger the disposable don't wait the realm response from concat and stop listening.
The workaround I find is to create a empty Object and return it like this :
if (response.code() == Integer.valueOf(ErrorUtils.ERROR_CODE_304)) {
return Single.just(new Config());
}
This trigger the disposable consumer with an empty object and I can after get the realm result with the good value because the throwable is not trigger.
But I don't want to receive this empty result I can do nothing with it and I need for all request to check if the content is not null like this :
.subscribe(new Consumer<Config>() {
#Override
public void accept(Config config) throws Exception {
if (config.getContent != null){
ProfileManager.getInstance().setConfig(config);
}
}
}
How can I return something with Single that don't trigger the consumer and the throwable ?
Thanks to EpicPandaForce the solution was to change the Single into Maybe like this :
.flatMap(new Function<Response<T>, Maybe<T>>() {
#Override
public Maybe<T> apply(#NonNull Response<T> response) throws Exception {
if (response.code() == Integer.valueOf(ErrorUtils.CODE_200)) {
return Maybe.just(response.body());
} else if (response.code() == Integer.valueOf(ErrorUtils.ERROR_CODE_304)) {
return Maybe.empty();
} else {
return Maybe.error(new Throwable(String.valueOf(response.code())));
}
}
});

SocketException unsubscribing request without onError

I'm trying to make a request using RxJava and Retrofit(2.3). I'm expecting that in case of any error I can retry or show a message to the client.
However, I notice that sometimes I have a SocketException which results in not calling onError, apparently the subscriber of the request just unsubscribes without calling anything else (not onComplete neither onError). Anyone knows why this is happening and how can I solve this in a generic way (without simply doing onUnsubscribe() and checking if the observable did not send any onError or onComplete)?
On my interface I have something like this:
#GET("userInfo")
Observable<List<UserInfo>> getUserInfo(#Header("token") String token);
This is how I create my observable:
public Observable<UserModel> requestUserInfo(final String token) {
return mService.getUserInfo(token)
.retryWhen(new RetryWithDelay(HTTP_RETRIES), HTTP_TIME_BETWEEN_RETRIES)))
.flatMap(new Func1<List<UserInfo>, Observable<UserModel>() {
#Override
public Observable<UserModel> call(List<UserInfo> userInfo) {
return Observable.just(new UserModel(userInfo));
}
});
}
------ UPDATE -------
This is how I call the requestUserInfo method on my presenter
private CompositeSubscription mCompositeSubscription = null;
public PresenterX(ViewX view) {
...
mCompositeSubscription = new CompositeSubscription();
}
public void getUserModel() {
String userToken = new AccessModel().getUserToken();
mCompositeSubscription.add(mNetworkRequestModel.requestUserInfo(userToken)
.flatMap(new Func1<UserModel, Observable<UserModel>>() {
#Override
public Observable<UserModel> call(UserModel userModel) {
if (userModel != null) {
saveUserModel(userModel); //sync saving
return Observable.just(userModel);
} else {
return Observable.error(new SaveException());
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<UserModel>() {
#Override
public void onCompleted() {
Log.i(TAG, "Subscriber was completed")
}
#Override
public void onError(Throwable e) {
Log.i(TAG, "Subscriber called onError")
mView.handleErrors(e);
}
#Override
public void onNext(UserModel userModel) {
Log.i(TAG, "Subscriber called onNext")
mView.populateUserInfo(userModel);
}
}));
}
//called by activity when onDestroyMethod is called
//I assume this is not called as I have other requests running parallelly to this getUserModel() and they are not terminated, despite having other compositeSubscription to manage those
public void onDestroy(){
mCompositeSubscription.clear();
}
As I have a HttpLoggingInterceptor, this is the only log printed to me while the request suddenly stops.
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
#Override
public void log(String message) {
LOG.info(Thread.currentThread(), String.format("%s", message));
}
});
Here is the log exception:
I/ExampleApp-ApiClient(21338): : Thread: 1343 | <-- HTTP FAILED: java.net.SocketException: Socket closed
Because you try to get rx object (Observable) from server. You have incorrect function
Remove Observable
#GET("userInfo")
List<UserInfo> getUserInfo(#Header("token") String token);

How to add refresh token logic to RxJava+Retrofit? [duplicate]

This question already has answers here:
retrofit with rxjava handling network exceptions globally
(2 answers)
Closed 6 years ago.
I'm new in RxJava, but I like it. And now I have little problem.
I'm using RxJava + Retrofit.
If I have valid token for user, I get response for API, but if token is invalid, I must refresh token and try make request again.
Valid token:
Make reuqest
Get response
Done
Invalid token:
Make request
Get response
If response_code == 403, need refresh token
Refresh token
Here I want go to 1 step and make request to API again
Else - done
How to make it using RxJava?
Try this one has my knowledge
service.normalRequest()
.flatMap( new Func1<Response, Observable<Response>>() {
#Override
public Observable<Response> call(Response response) {
if (response.code() == 403) {
return service.refreshToken(refreshRequest)
.flatMap(new Func1<Response, Observable<Response>>() {
#Override
public Observable<Response> call(Response response) {
return service.normalRequest();
}
});
} else {
return Observable.just(response);
}
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Subscriber<Response>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Response response) {
}
});
There is a special operator that can be used in this case:
public final Observable<T> retryWhen(final Func1<? super Observable<? extends Throwable>, ? extends Observable<?>> notificationHandler)
If you implement it you will see what parameters it accepts:
observable.retryWhen(new Func1<Observable<? extends Throwable>, Observable<T>>() {
#Override
public Observable<T> call(final Observable<? extends Throwable> error) {
}
});
You can see, that you get the error that was thrown by OkHttp. It is always throwing HttpException, if you call:
httpException.code()
You will get HTTP code number.
So the implementation of the function above might look like:
observable.retryWhen(new Func1<Observable<? extends Throwable>, Observable<T>>() {
#Override
public Observable<T> call(final Observable<? extends Throwable> error) {
return error.flatMap(doRelogging());
}
});
I have made a small library, that does what you expect:
RetrofitRxErrorHandler
You might try it or just look into sources for more advanced retry strategies regarding fetching API errors.

Retrofit cancel request

I'm using Retrofit 2.0. To get some data from RESTFull services I use procedures like this:
public Call downloadUser() {
// Create RetrofitService
Call<User> call = service.getUser();
call.enqueue(new Callback<User>() {
#Override
public void onResponse(Response<User> response, Retrofit retrofit) {
// Do some operations with User obj if response.isSuccess()
}
#Override
public void onFailure(Throwable t) {
// Failure
}
});
return call;
}
In some cases, I need to cancel my request. I use call.cancel(), but even if I call this procedure Callback.onResponse(...) or Callback.onFailure(...) triggered anyway, so using Call.cancel() doesn't cancel my request and it keeps on going until failure or response.
To know if a call was canceled or if it was truly successful you'll need to two a few things.
First it looks like the version of Retrofit2 you are using needs to be updated
Second you can check if a call was canceled based on the code below. Note that this will deal with cancels from the Call<> or the Dispatcher.class in OKHttp3
#Override
public void onResponse(Response<User> response, Response response) {
if(response != null && response.isSuccessful()) {
//Do Stuff with the response
}
}
#Override
public void onFailure(Call<User> user, Throwable t) {
if(user.isCanceled() || "Canceled".equals(t.getMessage())) {
//Call was canceled
} else {
//Call failed
}
}

Categories

Resources