1

I am using retorift to hit getAricle api and get list of articles related to the user. getArticle api will throw error if token passed is expired if so then I have to call refreshToken api to get new token then again I have to call the getArticle api

 ApiController.createRx().getArticle(token)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ response -> toast(response.body().url) }, { e ->
                println(e.printStackTrace())
                if(e is HttpException && e.code() in  arrayOf(401,403)){                      
                   //Here I want to call refresh tolken api
                   toast("Auth error")
                }
                else
                   toast(R.string.something_went_wrong)
            })

Edit

Even though given answers showed some direction but those are not a direct answer to my question. This is how solved it but I feel this can be refactored into much better code

ApiController.createRx().getArticle(Preference.getToken())
            .flatMap { value ->
                if (value.code() in arrayOf(403, 401)) {
                    ApiController.refreshToken()
                    ApiController.createRx().getArticle(Preference.getToken())
                } else Observable.just(value)
            }
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ response -> println("Success") }, { e ->
                e.printStackTrace()
                toast(R.string.something_went_wrong)
            })



fun refreshToken() {
        val token:String?=ApiController.createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst()?.body()?.token
        if (token != null) Preferences.setAuthToken(token)
    }

EDIT

I refactored my code to little more cleaner version

Observable.defer { ApiController.createRx().getArticle(Preferences.getToken()) }
            .flatMap {
                if (it.code() in arrayOf(401, 403)) {
                    ApiController.refreshToken()
                    Observable.error(Throwable())
                } else Observable.just(it)
            }
            .retry(1)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({println("Success") }, {
              it.printStackTrace()
              toast(R.string.something_went_wrong)
            })



 fun refreshToken() {
        var token: String? = null
        try {
            token = createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst().body()!!.token
        } catch (e: Exception) {
            throw e
        }
        println("saving token")
        if (token != null) Preferences.setAuthToken(token)
    }

EDIT

Please check my answer for the final refactored code

Praveena
  • 6,340
  • 2
  • 40
  • 53
  • First of all, with retrofit you can avoid going directly to `onError`. You can return `Single>` which never returns error. But if you want to keep getting `onError` try using the error handling operators of RxJava [here](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators) – masp Aug 17 '17 at 16:32
  • @masp In both the scenario I am not able get my head around how to recall getArticle api. Can you please show me how to do that – Praveena Aug 17 '17 at 17:59
  • Were you able to solve your problem? Were any of the answers provided helpful? If so, please consider upvoting any helpful answers, and if one of them led you to a solution, accepting that answer. – kenny_k Aug 23 '17 at 10:16
  • @theFunkyEngineer Hey I am able to solve it after spending some time And I wrote it as answer Please comment your suggestions – Praveena Aug 29 '17 at 05:16

4 Answers4

0

I have implemented this exact thing. Here is a slightly modified version of that code:

private Observable<Object> refreshTokenIfNotAuthorized(Observable<? extends Throwable> errors) {
    final AtomicBoolean alreadyRetried = new AtomicBoolean(false);

    return errors.flatMap(error -> {

        boolean isAuthorizationError = /* some logic analyzing each error*/ ;

        if (isAuthorizationError && !alreadyRetried.get()) {
            try {
                alreadyRetried.set(true);
                String newToken = federatedTokenRefresher.refreshToken()
                                                         .toBlocking()
                                                         .first();

                setLogin(newToken);
                return Observable.just(null);

            } catch (Exception e) {
                return Observable.error(error);
            }

        }
        return Observable.error(error);
    });
}

You can use this method like so:

doSomethingRequiringAuth().retryWhen(this::refreshTokenIfNotAuthorized);
kenny_k
  • 3,831
  • 5
  • 30
  • 41
  • @Praveen I agree that your solution works, but as you say, not exactly elegant. Is there something in my proposal that doesn't meet your need? If so, what is it? – kenny_k Aug 19 '17 at 19:54
  • at first I thought retrofit throwing all the 4xx errors to `onError` later I realized it is only network error which was directly going to `onError` so then `retryWhen` will not work. I had to use `flatMap` only. In your code how do you unsubscribe from this `federatedTokenRefresher.refreshToken() .toBlocking() .first()` – Praveena Aug 20 '17 at 04:31
  • You don't need to subscribe/unsubscribe when you call `toBlocking`. – kenny_k Aug 21 '17 at 08:39
  • What happens if I close the app in the middle of that blocking call? – Praveena Aug 21 '17 at 09:12
  • If by "close" you mean put in the background, it keeps running. Of course you should unsubscribe the UI as part of your lifecycle handling. As for your question about handling network errors - I would suggest you put another `retryWhen` operator in your chain before `.retryWhen(this::refreshTokenIfNotAuthorized)` and handle network errors there. Also in general, if the operation you're trying to perform is composed of multiple API calls, it might be easier to just ask the user to retry if there is a network error. – kenny_k Aug 21 '17 at 09:41
0

What kind of error you will received?. It´s seems like you could use onErrorResumeNext operator.

This operator once that receive a throwable, allow you to return an Observable instead the throwable in the onError

@Test
    public void observableOnErrorResumeException() {
        Integer[] numbers = {0, 1, 2, 3, 4, 5};

        Observable.from(numbers)
                .doOnNext(number -> {
                    if (number > 3) {
                        try {
                            throw new IllegalArgumentException();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }

                })
                .onErrorResumeNext(t -> Observable.just(666))
                .subscribe(System.out::println);

    }

You can see more examples here https://github.com/politrons/reactive/blob/master/src/test/java/rx/observables/errors/ObservableExceptions.java

paul
  • 12,873
  • 23
  • 91
  • 153
0

I will give you another option using groupBy operator

/**
 * In this example we create a response code group.
 */
@Test
public void testGroupByCode() {
    Observable.from(Arrays.asList(401,403, 200))
            .groupBy(code -> code)
            .subscribe(groupByCode -> {
                switch (groupByCode.getKey()) {
                    case 401: {
                        System.out.println("refresh token");
                        processResponse(groupByCode);
                        break;
                    }
                    case 403: {
                        System.out.println("refresh token");
                        processResponse(groupByCode);
                        break;
                    }
                    default: {
                        System.out.println("Do the toast");
                        processResponse(groupByCode);
                    }
                }
            });
}

private void processResponse(GroupedObservable<Integer, Integer> groupByCode) {
    groupByCode.asObservable().subscribe(value -> System.out.println("Response code:" + value));
}
paul
  • 12,873
  • 23
  • 91
  • 153
  • I am not able to understand this. This is too generic. How to apply this to my scenario and there is also no `asObservable` in Rx java2. And where are you re trying original observable after refreshing the token – Praveena Aug 20 '17 at 16:56
  • Take a look to the groupby operator in the documentation – paul Aug 20 '17 at 16:58
  • I know `groupBy` operator but how to apply that to my scenario – Praveena Aug 20 '17 at 16:59
  • As I did in my answer, your group contains the value of the response and also the observable. If it's 401-403 create a new observable with the refresh and the default just subscribe – paul Aug 20 '17 at 17:03
  • Not saying is better than your original solution by the way. Just different ;) – paul Aug 20 '17 at 17:04
  • Hey I solved my problem after spending some time on RxJava Please check my answer and please give me suggestion if something is not right in that – Praveena Aug 29 '17 at 05:14
0

I solved my problem after reading more about RxJava and this is how I implemented it. First of all will retrofit throw 4xx error to onError or onNext\onSuccess depends on how we define it. Ex:

@GET("content") fun getArticle(@Header("Authorization") token: String):Single<Article>

this will throw all the 4xx errors to onError and instead of Single<Article> if you define it as Single<Response<Article>> then all the response from server including 4xx will go to onNext\onSuccess

Single.defer { ApiController.createRx().getArticle(Preferences.getAuthToken())}
                .doOnError {
                    if (it is HttpException && it.code() == 401)
                        ApiController.refreshToken()
                }
                .retry { attempts, error -> attempts < 3 && error is HttpException && error.code() == 401 }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({println("Success") }, {
                  it.printStackTrace()
                  toast(R.string.something_went_wrong)
                })

I am using defer as a wrapper around my actual Observable because I want to recreate the article fetch observable on retry after token refresh because I want Preferences.getAuthToken() to be called again as my refresh token code stores newly fetched token in preference.

retry returns true if the HttpException is 401 and not attempted retry more than 2 times

Praveena
  • 6,340
  • 2
  • 40
  • 53