Moya rxswift: обновить токен и перезапустить запрос

Я использую Moya Rx swift, и я хочу поймать ответ, если код состояния 401 или 403, затем вызвать запрос токена обновления, затем снова вызвать / повторить исходный запрос, и для этого я выполнил этот Link, но я немного изменил его в соответствии со своими потребностями

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

И мой протокол:

import RxSwift

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

Но он не работает, и код не компилируется, я получаю следующее:

«Наблюдаемый» нельзя преобразовать в «Наблюдаемый ‹_›»

Я просто говорю о своих первых шагах к RX-swift, так что это может быть просто, но я не могу понять, что не так, за исключением того, что мне нужно вернуть тип, отличный от того, который я возвращаю, но я не знаю, как и где сделать так.

Ваша помощь очень ценится, и если у вас есть лучшая идея для достижения того, что я пытаюсь сделать, вы можете предложить ее.

Заранее спасибо за помощь.


person Ahmad Mahmoud Saleh    schedule 27.07.2018    source источник
comment
Где ошибка сборки? Возможно, вы захотите добавить явный тип возвращаемого значения в свой retryWhen, чтобы выявить основную проблему.   -  person Shai Mishali    schedule 30.07.2018
comment
Спасибо за ваш комментарий, я решил это, теперь я хочу перезапустить свой запрос после успешного запроса обновления токена, вы знаете, как это сделать без sup classing Moya   -  person Ahmad Mahmoud Saleh    schedule 30.07.2018


Ответы (3)


Вы можете выполнить перечисление при ошибке и вернуть тип String из вашего flatMap. Если запрос выполнен успешно, он вернет строку, иначе вернет наблюдаемую ошибку

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)
                    }
            }
        }
    }
person Suhit Patil    schedule 30.07.2018
comment
Замечательно, но есть проблема, запрос вызывает себя бесконечно и не останавливается в любой точке + знаете ли вы, как перезапустить первый запрос, если запрос токена обновления (т.е. результат достигает плоской карты) завершился успешно ?! Еще раз спасибо :) - person Ahmad Mahmoud Saleh; 30.07.2018
comment
ваш вопрос непонятен. у нас есть защитное условие, чтобы проверить, если индекс ‹= 3, поэтому он не должен работать бесконечно. Где вы хотите перезапустить запрос? - person Suhit Patil; 30.07.2018
comment
внес некоторые изменения в ответ. вы можете заранее проверить код состояния, а затем запросить токен, иначе отправьте ошибку в потоке. - person Suhit Patil; 30.07.2018
comment
Я хочу перезапустить запрос, если запрос на обновление токена завершился успешно - person Ahmad Mahmoud Saleh; 30.07.2018
comment
Я хочу перезапустить исходный запрос после успешного вызова запроса токена обновления - person Ahmad Mahmoud Saleh; 30.07.2018
comment
да, все еще не работает со мной, но я немного подправил его, чтобы он работал. - person Ahmad Mahmoud Saleh; 30.07.2018
comment
Теперь в этой части кода `.flatMapLatest {(responseString: String) -› Observable ‹String› в sessionServiceDelegate.refreshToken (responseString: responseString) return Observable.just (responseString)} `он останавливает выполнение и возвращает ошибку, если токен обновлен успешно, я хочу перезапустить свой исходный запрос после успешного обновления токена - person Ahmad Mahmoud Saleh; 30.07.2018

Наконец, я смог решить эту проблему, выполнив следующие действия:

Сначала создайте такой протокол (эти функции являются обязательными, а не дополнительными).

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

Очень-очень важно соответствовать протоколу SessionProtocol в классе, в котором вы пишете свой сетевой запрос (-ы) следующим образом:

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

Затем я создал функцию, которая возвращает 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)
                    }
            }
        }
}

Что делает эта функция, так это то, что она улавливает ошибку из ответа, а затем проверяет код состояния. Если это что-то другое, кроме 401, она вернет эту ошибку в блок onError исходного запроса, но если это 401 (вы можете изменить его чтобы удовлетворить ваши потребности, но это стандарт), тогда он будет выполнять запрос токена обновления.

После выполнения запроса токена обновления он проверяет ответ.

= ›Если код состояния больше или равен 400, это означает, что запрос токена обновления тоже не удался, поэтому верните результат этого запроса в исходный блок OnError запроса. = ›Если код состояния находится в диапазоне 200..300, это означает, что запрос на обновление токена выполнен успешно, следовательно, он повторит исходный запрос еще раз, если исходный запрос снова завершится ошибкой, тогда сбой перейдет к блоку OnError как обычно.

Примечания:

= ›Очень важно проанализировать и сохранить новый токен после того, как запрос токена обновления будет успешным и будет возвращен новый токен, поэтому при повторении исходного запроса он будет делать это с новым токеном, а не со старым.

Ответ токена возвращается в этом обратном вызове прямо перед повторением исходного запроса. func tokenDidRefresh (response : String)

= ›В случае сбоя запроса на обновление токена может случиться так, что срок действия токена истек, поэтому в дополнение к тому, что сбой перенаправляется на onError исходного запроса, вы также получаете этот обратный вызов ошибки func didFailedToRefreshToken(), вы можете использовать его, чтобы уведомить пользователя о том, что его сеанс потеряно или выйти из системы, или что-то в этом роде.

= ›Очень важно вернуть функцию, которая выполняет запрос токена, потому что это единственный способ, которым функция refreshAuthenticationTokenIfNeeded знает, какой запрос вызвать для выполнения токена обновления.

func getTokenRefreshService() -> Single<Response> {
    return apiProvider.rx.request(.doRefreshToken())
}
person Ahmad Mahmoud Saleh    schedule 11.08.2018

Вместо написания расширения для Observable есть другое решение. Он написан на чистом RxSwift и в случае неудачи возвращает классическую ошибку.

Простой способ обновить токен сеанса Auth0 с помощью RxSwift и Moya

Основным преимуществом решения является то, что оно может быть легко применимо для различных сервисов, подобных Auth0, позволяющих аутентифицировать пользователей в мобильных приложениях.

person datarocker    schedule 24.09.2018
comment
вы должны ответить на заданный вопрос, не рекомендуя другие способы решения проблемы, - person MohammadReza Alagheband; 24.09.2018
comment
Нравится и большое спасибо, но исходный запрос не перезапустится сам после обновления токена, и мне также придется писать CustomMoyaProvider, но с использованием моего кода он будет работать с любым поставщиком Moya, или я что-то упускаю? - person Ahmad Mahmoud Saleh; 24.09.2018