I have token to make requests with it, this token have some lifeTime.
So when I get 401 error, I need to refreshToken with other request.
I want to make it smoothly for user(without any interruptions): if get 401, than make refresh, and after it repeat first request with new available token.
here is my code:
#POST("/refresh_token")
Observable<Auth> refreshToken(#Body BodyAuth body);
#POST("/someLink")
Observable<Answer> someRequest(#Body OtherBody body);
Observable<Answer> makeSomeRequest(){
retutn someRequest(new Body(currentToken)).
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(StatusCodeVerifier())
.retryWhen(RetryWithNewToken(requestManager))
}
public class StatusCodeVerifier<T> implements Observable.Transformer<Response<T>,T> {
#Override
public Observable<T> call(Observable<Response<T>> responseObservable) {
return responseObservable.
flatMap(new Func1<Response<T>, Observable<?>>() {
#Override
public Observable<?> call(Response<T> tResponse) {
if (tResponse.code() == 401){
throw new UnAuthorizedException("custom exception");
} else {
return Observable.just(tResponse);
}
}
});
}
}
public class RetryWithNewToken implements Func1<Observable<Throwable>, Observable<?>> {
private UserDataManager requestManger;
public RetryWithNewToken(RequestManager requestManger){
this.requestManger = requestManger;
}
#Override
public Observable<?> call(final Observable<Throwable> attempts) {
return attempts.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(final Throwable throwableObservable) {
if (throwableObservable instanceof UnAuthorizedException){
return requestManger. refreshToken().subscribe(new Action1<CbAuth>() {
#Override
public void call(TokenData newToken) {
updateToken(newToken);
//here i try to repeat previous request that was failed because of 401 error
Observable.just(attempts);
}
});
}
return Observable.error(throwableObservable);
}
});
}
}
every time after successful refreshToken previous request called, but it is called with invalid (old one) data.
So how can I repeat request with new token data?
It's because of you initialize your someRequest Observable with its arguments only once. And when an error occurs you just resubscribes to it. But you have to poll currentToken on each subscription. It could be done by using Observable.fromCallable operator:
Observable<Answer> makeSomeRequest(){
return Observable.fromCallable(() -> currentToken) // called on each subscription
.flatMap(token -> someRequest(new Body(token)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(StatusCodeVerifier())
.retryWhen(RetryWithNewToken(requestManager))
}
Related
I'm trying to convert a callback hell to RX but I'm stuck with getting the proper order, below is my functionality I want to achieve
a) User Login-> get the Auth Cookies, if login credentials invalid show error message
b) use the Auth Cookies to get Customer Type,
c) if the Customer Type is zero/ show profile Restricted Error Message and log out the user
d) if the customerType, not zero proceed to get the other customer Details
e) if any of the customer APIs returns an error response, log out the user and show login failure message
f) if all customer API success show the home screen
API
Login
#FormUrlEncoded
#POST("distauth/UI/Login")
Single<Response<Void>> doLogin1(#Field("username") String username, #Field("password") String password,
#Field("rememberme") String rememberMe, #Field("answer") String answer,
#QueryMap Map<String, String> options);
public Single<Boolean> doLogin(#NonNull String username, #Nullable String password) {
return authapi.doLogin1(username, password, "y", "", logiOptions)
.flatMap(new Function<Response<Void>, SingleSource<Boolean>>() {
#Override
public SingleSource<Boolean> apply(Response<Void> response) throws Exception {
if (response.code() == HttpStatus.MOVED_TEMPORARILY.value()
&& !StringUtils.isEmpty(Session.getCookie())
) {
return Single.just(true);
}
throw new Exception("Invalid Login Details");
}
});
}
//==========
Logout
#FormUrlEncoded
#POST("distauth/UI/Logout")
#Headers("Cache-Control: no-cache")
Completable doLogout(#Field("logout") boolean logout); //return 302 HTTP Status code with empty iPlanetCookie
//==========
NOTE: Loing/logout is not a REST API, this legacy app implement as Form Post ;) so when the success of login return 302 with cookies, and log out also return 302 as status code
Get Customer Details
Single<CustomerAccountVO> getCustomerAccountDetails(boolean forceRefresh);
//==========
Single<CustomerType> getCustomerUserProfile(boolean forceRefresh);
#Override
public Single<CustomerType> getCustomerUserProfile(boolean applyResponseCache) {
return this.mCustomerRemoteDataStore.getCustomerUserProfile(applyResponseCache)
.doOnSuccess(new Consumer<CustomerType>() {
#Override
public void accept(CustomerType customerType) throws Exception {
if (customerType != null && customerType.getBody() != null &&
!StringUtils.isEmpty(customerType.getBody())) {
if (customerType.getBody().equalsIgnoreCase(AppConfig.ERROR)) {
throw new CustomerProfileNotFound(500, "user account restrictions");
} else {
mCustomerLocalRepository.saveCustomerType(customerType);
}
}
}
}).doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "error occurred while getting customer user profile", throwable);
}
});
}
//==========
Single<CustomerAccountId> getAccountId(boolean forceRefresh);
//==========
Single<Customer> getCustomer(boolean forceRefresh);
//==========
Get Customer Full Details
Single<CustomerDetails> getCustomerFullDetails(boolean applyResponseCache);
Implementation:
#Override
public Single<CustomerDetails> getCustomerFullDetails(boolean forceRefresh) {
Single<CustomerDetails> customerDetails = Single.zip(
getCustomerUserProfile(forceRefresh).subscribeOn(Schedulers.io()),
getAccountId(forceRefresh).subscribeOn(Schedulers.io()),
getCustomerAccountDetails(false).subscribeOn(Schedulers.io()),
getCustomer(forceRefresh).subscribeOn(Schedulers.io()), new Function4<CustomerType, CustomerAccountId,
CustomerAccountVO, Customer, CustomerDetails>() {
#Override
public CustomerDetails apply(#NonNull CustomerType customerType,
#NonNull CustomerAccountId customerAccountId,
#NonNull CustomerAccountVO customerAccountVO,
#NonNull Customer customer) throws Exception {
return CustomerDetails.builder().customerType(customerType).customerAccountVO
(customerAccountVO).customer(customer).customerAccountId(customerAccountId).
build();
}
});
return customerDetails;
}
//==========
Each customer request is independent so I thought to execute as sperate thread and zip the final result/
Single<BaseServerResponse> updateCustomerDetails(#Nonnull boolean secure, int secureRequestCode, #Nonnull JSONObject customerContact);
//Presenter Implementation: this implementation not working as i expect above, can some one help me to get this correct,
public void doLoginHandler(#NonNull String username, #NonNull String password) {
checkViewAttached();
getMvpView().showLoadingIndicator();
addSubscription(
apiService.doLogin2(username, password)
.subscribeOn(Schedulers.io())
.flatMap(new Function<Boolean, SingleSource<CustomerDetails>>() {
#Override
public SingleSource<CustomerDetails> apply(Boolean aBoolean) throws Exception {
if (aBoolean) {
//get customr Full Details
Log.d(TAG, "apply: "+aBoolean);
return customerRepository.getCustomerFullDetails(true);
}
return null;
}
}).observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(new Function<Throwable, SingleSource<? extends CustomerDetails>>() {
#Override
public SingleSource<? extends CustomerDetails> apply(Throwable throwable) throws Exception {
if (throwable instanceof CustomerProfileNotFound) {
getMvpView().showUserProfileAccessRestrictMessage();
} else {
getMvpView().onLoginAuthFailure();
}
return Single.just(CustomerDetails.builder().errorOccurred(true).build());
}
})
.flatMapCompletable(new Function<CustomerDetails, CompletableSource>() {
#Override
public CompletableSource apply(CustomerDetails customerDetails) throws Exception {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout();
}
return Completable.complete();
}
})
.subscribe(new Action() {
#Override
public void run() throws Exception {
getMvpView().onLoginAuthSuccess();
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
if (throwable instanceof CustomerProfileNotFound) {
getMvpView().showUserProfileAccessRestrictMessage();
} else {
getMvpView().onLoginAuthFailure();
}
}
}));
}
First I'll state the problem with your code.
.flatMapCompletable(new Function<CustomerDetails, CompletableSource>() {
#Override
public CompletableSource apply(CustomerDetails customerDetails) throws Exception {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout();
}
return Completable.complete();
}
})
This chain observable (which is the one you subscribe to) is always going to give a Completed state unless a network error happens when calling the logout API, that's because you either return the logout Completable or an instant Completable.
Secondly, I think the solution is in logically sorting everything out, the key to error handling in such a case would be creating a different Exception for each error case with it's own error message,
it can go like this (I'm just using the logical names, hopefully that will give you the idea):
loginObservable.flatMap { authCredentials -> {
if (authCredentials.isValid())
return getCustomerTypeObservable(authCredentials)
else
return Single.error(InvalidCredentialsException("message goes here (optional)"))
}}.flatMap { type -> {
if (type == 0)
return Single.error(ProfileRestrictedException("different message maybe?"))
else
return getCustomerDetailsZippedObservable(type)
}}
/* ..etc */
Then at the subscription site you do something like:
myObservable.subscribe( {
/* Handle success*/
}, { exception ->
when(exception) {
is InvalidCredentialsException -> mvpView.showError(message)
is ProfileRestrictedException -> {
mvpView.showError(message)
logout()
}
else -> /* Handle an exception that is not listed above */
}
} )
This way IMO is more convenient than using onErrorResumeNext.
EDIT: You can also overcome the issue stated above by doing something like:
.flatMapCompletable { customerDetails -> {
if(customerDetails.isErrorOccurred()){
return apiService.doLogout()
.then(Completable.error(LoginFailedException("Message"))) /* This will guarantee the stream terminates with the required error type after logout is successful */
} else {
return Completable.complete()
}
}}
I use retrofit2 with rxjava extension.
I have a list of REST API urls and want to do this:
for each
check whether a corresponding file locally exists
if yes: call the API and store the response or the HTTP error
if not: store a customized error
return the list of those results
My problem is: apply returns (with an empty RequestResult) before the server response is received. I think, I understand why, but I don't know how to fix it, because I need to return a RequestResult and not the Retrofit observable.
How can this be solved?
Here is my code:
#GET
Observable<Response<ResponseBody>> enroll(#Url String url);
class RequestResult {
CustomException error;
Response<ResponseBody> response;
}
Observable<ClassOfListItem> observable = Observable.fromIterable(listOfItems);
observable
.flatMap(new Function<ClassOfListItem, ObservableSource<RequestResult>>() {
#Override
public ObservableSource<RequestResult> apply(ClassOfListItem listItem) throws Exception {
RequestResult requestResult = new RequestResult();
if (fileExists(listItem.url)) {
Observable<Response<ResponseBody>> callObservable = restAPI.enroll(listItem.url)
.subscribeOn(Schedulers.io());
callObservable
.subscribe(new DisposableObserver<Response<ResponseBody>>() {
#Override
public void onNext(Response<ResponseBody> responseBodyResponse) {
onPremiseEnrollmentResult.response = responseBodyResponse;
}
#Override
public void onError(Throwable e) {
onPremiseEnrollmentResult.error = new CustomException(e);
}
#Override
public void onComplete() {
}
});
}
else {
requestResult.error = new CustomException("file not found");
}
return Observable.just(requestResult);
}
}
.toList()
.observerOn(AndroidScheduler.mainThread())
.subscribe(new DisposableSingleObserver<List<RequestResult>>() {
#Override
public void onError(Throwable e) {
Log.d("onError", e.getMessage());
}
#Override
public void onSuccess(List<RequestResult> requestResults) {
// parse results
}
}
)
The flatMap() operator allows you to turn one observable into a different observable. You have a nested observer chain inside your apply() which is not part of the observer chain, so it will be empty because it has not completed yet.
To fix this, when the file exists, return the observable.
observable
.flatMap(new Function<ClassOfListItem, ObservableSource<RequestResult>>() {
#Override
public ObservableSource<RequestResult> apply(ClassOfListItem listItem) throws Exception {
RequestResult requestResult = new RequestResult();
if (fileExists(listItem.url)) {
return restAPI.enroll(listItem.url)
.subscribeOn(Schedulers.io());
}
return Observable.error( new CustomException("file not found") );
}
}
.toList()
.observerOn(AndroidScheduler.mainThread())
.subscribe(new DisposableSingleObserver<List<RequestResult>>() {
#Override
public void onError(Throwable e) {
Log.d("onError", e.getMessage());
}
#Override
public void onSuccess(List<RequestResult> requestResults) {
// parse results
}
}
If you need to capture both errors and successes into the list, then you can add map() operator to wrap RequestResult around the response and onErrorResumeNext() to wrap RequestResult around the error before the toList() operator.
If you are making api call on background thread then what you can do is invoke it synchronously....in your case your retrofit api method would change to following
Call<Response<ResponseBody>> enroll(#Url String url);
and you'd invoke by calling restAPI.enroll(listItem.url).execute()
I have some code that first has to run on AndroidSchedulers.mainThread(), then has to do a HTTP request, so has to run on Schedulers.io(), and handle the result on UI, so back to AndroidSchedulers.mainThread().
I receive InterruptedIOException when switching from AndroidSchedulers.mainThread() to Scheulers.io().
Here's some code:
Model model = getModel();
Completable.fromAction(
new Action0() {
public void call() {
mSubject.onNext(model)
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
...
public <T> Single<T> fetchFromServer() {
Request request = new Request(); // some request from server, not important
return bodyFrom2(request);
}
public <T> Single<T> bodyFrom2(final Request<T> request) {
return Single.defer(new Callable<Single<T>>() {
#Override
public Single<T> call() throws Exception {
try {
Response<T> response = request.execute();
if (response.error() != null)
return Single.error(response.error().getMessage());
else {
return Single.just(response.body());
}
} catch (IOException e) {
return Single.error(e);
}
}
});
}
public static <T> Single<T> bodyFrom1(final Request<T> request) {
return Single.create(new Single.OnSubscribe<T>() {
#Override
public void call(SingleSubscriber<? super T> subscriber) {
try {
Response<T> response = request.execute();
if (subscriber.isUnsubscribed())
return;
if (response.error() != null)
subscriber.onError(response.error().getMessage());
else {
subscriber.onSuccess(response.body());
}
} catch (IOException e) {
if (subscriber.isUnsubscribed())
return;
subscriber.onError(e);
}
}
});
}
The exception is thrown in bodyFrom() (1 or 2), at request.execute().
I used bodyFrom1(), but I found this question on SO and thought about trying with the second one. Regardless, I receive the exception.
Trying to find what and where the problem is, I tried this:
Completable.complete()
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
which still throws InterruptedIOException, and this:
Completable.complete()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.io())
.andThen(fetchFromServer())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* handle success and error */);
which works fine.
EDIT:
It seems to work if I'm using Observable or Single instead of Completable.
Added an issue on RxAndroid's Github.
I'm pretty new to RxJava and Retrofit and am trying to write my API calls with it. All the API calls return a JSON body on error which is in the general format as,
{"errors":[{"code":100, "message":"Login/Password not valid", "arguments":null}]}
Currently my code for the login API call (others are also similar) is,
mConnect.login(id, password)
.subscribe(new Subscriber<Token>() {
#Override
public void onCompleted() {
Log.d(TAG, "onCompleted()");
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError(): " + e);
if (e instanceof HttpException) {
// dump e.response().errorBody()
}
}
#Override
public void onNext(Token token) {
Log.d(TAG, "onNext(): " + token);
}
});
When I get an error at the onError(), I would like to automatically decode the JSON in the error body to a POJO instead and use that. Is there a way to do this preferably in one place for all other API calls. Any help is appreciated.
I would suggest the use of a reusable Transformer along with the onErrorResumeNext operator to encapsulate your logic. It'd look something like this:
<T> Observable.Transformer<T, T> parseHttpErrors() {
return new Observable.Transformer<T, T>() {
#Override
public Observable<T> call(Observable<T> observable) {
return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
#Override
public Observable<? extends T> call(Throwable throwable) {
if (throwable instanceof HttpException) {
HttpErrorPojo errorPojo = // deserialize throwable.response().errorBody();
// Here you have two options, one is report this pojo back as error (onError() will be called),
return Observable.error(errorPojo); // in this case HttpErrorPojo would need to inherit from Throwable
// or report this pojo back as part of onNext()
return Observable.just(errorPojo); //in this case HttpErrorPojo would need to inherit from <T>
}
// if not the kind we're interested in, then just report the same error to onError()
return Observable.error(throwable);
}
});
}
};
}
Pay attention to the comments in the code, since you have to make the decision whether you want to report the parsed response onError() or onNext().
Then you can use this transformer anywhere in your API calls like this:
mConnect.login(id, password)
.compose(this.<Token>parseHttpErrors()) // <-- HERE
.subscribe(new Subscriber<Token>() {
#Override
public void onCompleted() {
Log.d(TAG, "onCompleted()");
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError(): " + e);
if (e instanceof HttpErrorPojo) {
// this will be called if errorPojo was reported via Observable.error()
}
}
#Override
public void onNext(Token token) {
Log.d(TAG, "onNext(): " + token);
if (token instanceof HttpErrorPojo) {
// this will be called if errorPojo was reported via Observable.just()
}
}
});
Deserialize may be an issue too. You can use the retrofit converter to deserialize it (or do it yourself).
My solution adds a bit to the one from murki:
<T> Observable.Transformer<T, T> parseHttpErrors() {
return new Observable.Transformer<T, T>() {
#Override
public Observable<T> call(Observable<T> observable) {
return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
#Override
public Observable<? extends T> call(Throwable throwable) {
if ( throwable instanceof HttpException ) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SERVER_URL) // write your url here
.addConverterFactory(GsonConverterFactory.create())
.build();
Converter<ResponseBody, Error> errorConverter =
retrofit.responseBodyConverter(Error.class, new Annotation[0]);
// Convert the error body into our Error type.
try {
Error error = errorConverter.convert(((HttpException) throwable).response().errorBody());
// Here you have two options, one is report this pojo back as error (onError() will be called),
return Observable.error(new Throwable(error.getMessage()));
}
catch (Exception e2) {
return Observable.error(new Throwable());
}
}
// if not the kind we're interested in, then just report the same error to onError()
return Observable.error(throwable);
}
});
}
};
}
and then at the onError(),
#Override
public void onError(Throwable e) {
progressBar.setVisibility(View.GONE); // optional
if ( !TextUtils.isEmpty(e.getMessage()) ) {
// show error as you like
return;
}
// show a default error if you wish
}
So here's my code
RestAdapter restAdapter=new RestAdapter.Builder().setEndpoint(HelperFunctions.BASE_URL).build();
final ApiInterface apiInterface=restAdapter.create(ApiInterface.class);
apiInterface.submitDataToAnalyze("dataToAnalyze", "852741963", 1, uuid, dataToAnalyze.toString(), todayDate)
.onErrorReturn(new Func1<Throwable, BasicResponse>() {
#Override
public BasicResponse call(Throwable throwable) {
Log.e(FILE_NAME, "masuk sini??");
return null;
}
})
.flatMap(new Func1<BasicResponse, Observable<LocationSuggestion>>() {
#Override
public Observable<LocationSuggestion> call(BasicResponse basicResponse) {
Log.v(FILE_NAME, "basicResponse: " + basicResponse.toString());
return Observable.just(null);
}
})
.map(new Func1<LocationSuggestion, Integer>() {
#Override
public Integer call(LocationSuggestion suggestion) {
Log.v(FILE_NAME, "eh sampai sini");
return 1;
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
None of my log was displayed in the logcat. What should I do to know if my network call (and all other subsequant flatmap or map) is working correctly? Btw apiInterface.submitDataToAnalyze() should insert some data into a remote database (which in this case it didn't insert anything, hence I'm trying to figure out where the error happened)