简体   繁体   中英

Moya rxswift : Refresh token and restart request

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.

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)
                    }
            }
        }
    }

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())
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM