1

I'm using Moya Rx swift and i want to catch the response if the status code is 401 or 403 then call refresh token request then recall/retry the original request again and to do so i followed this Link but i tweaked it a bit to suit my needs

public extension ObservableType where E == Response {

/// Tries to refresh auth token on 401 errors and retry the request.
/// If the refresh fails, the signal errors.
public func retryWithAuthIfNeeded(sessionServiceDelegate : SessionProtocol) -> Observable<E> {
    return self.retryWhen { (e: Observable<Error>) in
        return Observable
                .zip(e, Observable.range(start: 1, count: 3),resultSelector: { $1 })
                .flatMap { i in
                           return sessionServiceDelegate
                                    .getTokenObservable()?
                                    .filterSuccessfulStatusAndRedirectCodes()
                                    .mapString()
                                    .catchError {
                                        error in
                                            log.debug("ReAuth error: \(error)")
                                            if case Error.StatusCode(let response) = error {
                                                if response.statusCode == 401 || response.statusCode == 403 {
                                                    // Force logout after failed attempt
                                                    sessionServiceDelegate.doLogOut()
                                                }
                                            }
                                            return Observable.error(error)
                                    }
                                    .flatMapLatest({ responseString in
                                        sessionServiceDelegate.refreshToken(responseString: responseString)
                                        return Observable.just(responseString)
                                    })
        }}
    }
}

And my Protocol :

import RxSwift

public protocol SessionProtocol {
    func doLogOut()
    func refreshToken(responseString : String)
    func getTokenObservable() -> Observable<Response>? 
}

But it is not working and the code is not compiling, i get the following :

'Observable' is not convertible to 'Observable<_>'

I'm just talking my first steps to RX-swift so it may be simple but i can not figure out what is wrong except that i have to return a type other than the one I'm returning but i do not know how and where to do so.

Your help is much appreciated and if you have a better idea to achieve what I'm trying to do, you are welcome to suggest it.

Thanks in advance for your help.

Community
  • 1
  • 1
Ahmad Mahmoud Saleh
  • 169
  • 1
  • 4
  • 15
  • Where is the build error? You might want to add an explicit return type to your `retryWhen` to reveal the underlying issue. – Shai Mishali Jul 30 '18 at 09:05
  • Thanks for your comment, i solved it, now i want to restart my request after token refresh request success, do you have any idea how to do this without sup classing Moya – Ahmad Mahmoud Saleh Jul 30 '18 at 09:38

3 Answers3

2

You can enumerate on error and return the String type from your flatMap. If the request succeeded then it will return string else will return error observable

public func retryWithAuthIfNeeded(sessionServiceDelegate: SessionProtocol) -> Observable<E> {
    return self.retryWhen { (error: Observable<Error>) -> Observable<String> in
        return error.enumerated().flatMap { (index, error) -> Observable<String> in
            guard let moyaError = error as? MoyaError, let response = moyaError.response, index <= 3  else {
                throw error
            }
            if response.statusCode == 401 || response.statusCode == 403 {
                // Force logout after failed attempt
                sessionServiceDelegate.doLogOut()
                return Observable.error(error)
            } else {
                return sessionServiceDelegate
                    .getTokenObservable()!
                    .filterSuccessfulStatusAndRedirectCodes()
                    .mapString()
                    .flatMapLatest { (responseString: String) -> Observable<String> in
                        sessionServiceDelegate.refreshToken(responseString: responseString)
                        return Observable.just(responseString)
                    }
            }
        }
    }
Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
  • Awesome but there is a problem, The request calls itself infinitely and not stopping at any point + do you have any idea how to restart the first request if the refresh token request (i.e the result reaches inside the flat map) succeeded ?! Thank you again :) – Ahmad Mahmoud Saleh Jul 30 '18 at 12:26
  • your question is not clear. we have guard condition to check if the index <= 3 so it should not run infinitely. Where you want to restart the request? – Suhit Patil Jul 30 '18 at 12:58
  • made some changes in response. you can check the status code upfront and then request for token else send the error in stream. – Suhit Patil Jul 30 '18 at 13:07
  • i want to restart the request if the token refresh request succeeded – Ahmad Mahmoud Saleh Jul 30 '18 at 15:37
  • I want to restart the original request after calling the refresh token request succeeded – Ahmad Mahmoud Saleh Jul 30 '18 at 15:39
  • yest, still not working with me but i tweaked it a bit to make it work. – Ahmad Mahmoud Saleh Jul 30 '18 at 16:11
  • Now at this part of the code ` .flatMapLatest { (responseString: String) -> Observable in sessionServiceDelegate.refreshToken(responseString: responseString) return Observable.just(responseString) } ` it stops execution and return error if the token refreshed successfully, i want to restart my original request after the token refreshes successfully – Ahmad Mahmoud Saleh Jul 30 '18 at 16:11
1

Finally i was able to solve this by doing the following :

First create a protocol like so ( Those functions are mandatory and not optional ).

import RxSwift
            
public protocol SessionProtocol {
    func getTokenRefreshService() -> Single<Response>
    func didFailedToRefreshToken()
    func tokenDidRefresh (response : String)
}

It is very very important to conform to the protocol SessionProtocol in the class that you write your network request(s) in like so :

import RxSwift
    
class API_Connector : SessionProtocol {
        //
        private final var apiProvider : APIsProvider<APIs>!
        
        required override init() {
            super.init()
            apiProvider = APIsProvider<APIs>()
        }
        // Very very important
        func getTokenRefreshService() -> Single<Response> {
             return apiProvider.rx.request(.doRefreshToken())
        }
        
        // Parse and save your token locally or do any thing with the new token here
        func tokenDidRefresh(response: String) {}
        
        // Log the user out or do anything related here
        public func didFailedToRefreshToken() {}
        
        func getUsers (page : Int, completion: @escaping completionHandler<Page>) {
            let _ = apiProvider.rx
                .request(.getUsers(page: String(page)))
                .filterSuccessfulStatusAndRedirectCodes()
                .refreshAuthenticationTokenIfNeeded(sessionServiceDelegate: self)
                .map(Page.self)
                .subscribe { event in
                    switch event {
                    case .success(let page) :
                        completion(.success(page))
                    case .error(let error):
                        completion(.failure(error.localizedDescription))
                    }
                }
        }
        
}

Then, I created a function that returns a Single<Response>.

import RxSwift
    
extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
        
        // Tries to refresh auth token on 401 error and retry the request.
        // If the refresh fails it returns an error .
        public func refreshAuthenticationTokenIfNeeded(sessionServiceDelegate : SessionProtocol) -> Single<Response> {
            return
                // Retry and process the request if any error occurred
                self.retryWhen { responseFromFirstRequest in
                    responseFromFirstRequest.flatMap { originalRequestResponseError -> PrimitiveSequence<SingleTrait, ElementType> in
                            if let lucidErrorOfOriginalRequest : LucidMoyaError = originalRequestResponseError as? LucidMoyaError {
                            let statusCode = lucidErrorOfOriginalRequest.statusCode!
                            if statusCode == 401 {
                                // Token expired >> Call refresh token request
                                return sessionServiceDelegate
                                    .getTokenRefreshService()
                                    .filterSuccessfulStatusCodesAndProcessErrors()
                                    .catchError { tokeRefreshRequestError -> Single<Response> in
                                        // Failed to refresh token
                                        if let lucidErrorOfTokenRefreshRequest : LucidMoyaError = tokeRefreshRequestError as? LucidMoyaError {
                                            //
                                            // Logout or do any thing related
                                            sessionServiceDelegate.didFailedToRefreshToken()
                                            //
                                            return Single.error(lucidErrorOfTokenRefreshRequest)
                                        }
                                        return Single.error(tokeRefreshRequestError)
                                    }
                                    .flatMap { tokenRefreshResponseString -> Single<Response> in
                                        // Refresh token response string
                                        // Save new token locally to use with any request from now on
                                        sessionServiceDelegate.tokenDidRefresh(response: try! tokenRefreshResponseString.mapString())
                                        // Retry the original request one more time
                                        return self.retry(1)
                                }
                            }
                            else {
                                // Retuen errors other than 401 & 403 of the original request
                                return Single.error(lucidErrorOfOriginalRequest)
                            }
                        }
                        // Return any other error
                        return Single.error(originalRequestResponseError)
                    }
            }
        }
}

What this function do is that it catches the error from the response then check for the status code, If it is any thing other than 401 then it will return that error to the original request's onError block but if it is 401 (You can change it to fulfill your needs but this is the standard) then it is going to do the refresh token request.

After doing the refresh token request, it checks for the response.

=> If the status code is in bigger than or equal 400 then this means that the refresh token request failed too so return the result of that request to the original request OnError block. => If the status code in the 200..300 range then this means that refresh token request succeeded hence it will retry the original request one more time, if the original request fails again then the failure will go to OnError block as normal.

Notes:

=> It is very important to parse & save the new token after the refresh token request is successful and a new token is returned, so when repeating the original request it will do it with the new token & not with the old one.

The token response is returned at this callback right before repeating the original request. func tokenDidRefresh (response : String)

=> In case the refresh token request fails then it may that the token is expired so in addition that the failure is redirected to the original request's onError, you also get this failure callback func didFailedToRefreshToken(), you can use it to notify the user that his session is lost or log him out or anything.

=> It is very important to return the function that do the token request because it is the only way the refreshAuthenticationTokenIfNeeded function knows which request to call in order to do the refresh token.

func getTokenRefreshService() -> Single<Response> {
    return apiProvider.rx.request(.doRefreshToken())
}
Community
  • 1
  • 1
Ahmad Mahmoud Saleh
  • 169
  • 1
  • 4
  • 15
-1

Instead of writing an extension on Observable there's another solution. It's written on pure RxSwift and returns a classic error in case of fail.

The easy way to refresh session token of Auth0 with RxSwift and Moya

The main advantage of the solution is that it can be easily applicable for different services similar to Auth0 allowing to authenticate users in mobile apps.

datarocker
  • 19
  • 4
  • you should answer on the question asked not recommending other ways of handling a problem, – MohammadReza Alagheband Sep 24 '18 at 12:53
  • Like it and thank you very much but the the original request wont restart it self after Token refresh and also I will have to Write `CustomMoyaProvider` but using my code, it will work with any Moya provider, or am i missing something ? – Ahmad Mahmoud Saleh Sep 24 '18 at 12:57