Использование retryWhen для обновления токенов на основе кода ошибки http

Я нашел этот пример на как обновить токен oauth с помощью moya и rxswift который мне пришлось немного изменить, чтобы скомпилировать. Этот код работает 80% для моего сценария. Проблема в том, что он будет работать для всех ошибок http, а не только для ошибок 401. Я хочу, чтобы все мои другие ошибки http передавались как ошибки, чтобы я мог обрабатывать их в другом месте и не глотать их здесь.

С этим кодом, если я получаю HttpStatus 500, он будет запускать код аутентификации 3 раз это явно не то, чего я хочу.

Ive попытался изменить этот код, чтобы обрабатывать только handle 401 ошибки, но кажется, что независимо от того, что я делаю, я не могу получить код для компиляции. Он всегда жалуется на неправильный тип возврата,"Cannot convert return expression of type Observable<Response> to return type Observable<Response>" что для меня не имеет смысла..

что я хочу: обрабатывать 401, но останавливаться на всех других ошибках

import RxSwift
import KeychainAccess
import Moya

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() -> Observable<E> {
    return self.retryWhen {
      (e: Observable<ErrorType>) in
      return Observable.zip(e, Observable.range(start: 1, count: 3), resultSelector: {  })
        .flatMap { i in
          return AuthProvider.sharedInstance.request(
            .LoginFacebookUser(
              accessToken: AuthenticationManager.defaultInstance().getLoginTokenFromKeyChain(),
              useFaceBookLogin: AuthenticationManager.defaultInstance().isFacebookLogin())
            )
            .filterSuccessfulStatusCodes()
            .mapObject(Accesstoken.self)
            .catchError {
              error in
              log.debug("ReAuth error: (error)")
              if case Error.StatusCode(let response) = error {
                if response.statusCode == 401 {
                  // Force logout after failed attempt
                  log.debug("401:, force user logout")
                  NSNotificationCenter.defaultCenter().postNotificationName(Constants.Notifications.userNotAuthenticated, object: nil, userInfo: nil)
                }
              }
              return Observable.error(error)
            }.flatMapLatest({
              token -> Observable<Accesstoken> in
              AuthenticationManager.defaultInstance().storeServiceTokenInKeychain(token)
              return Observable.just(token)
            })
      }
    }
  }
}

4 ответов


Ошибка Компиляции

в какой строке есть ошибка компиляции? Мне кажется, что это была бы такая строка:

.catchError {
    error in
    //...
    return Observable.error(error)  // is this the line causing the compilation error?
}

если так, то, вероятно, потому что catchError ожидает, что блок вернет Observable<Response>, С которым он может продолжить в случае ошибки, а не Observable<ErrorType>.

в любом случае это помогает аннотировать ваш код с большим количеством типов, чтобы вы могли точно определить такие проблемы, а также помочь компилятору Swift, который часто не может понять все это само по себе. Так что что-то вроде этого помогло бы вам:

.catchError {
    error -> Observable<Response> in
    //...
    return Observable.error(error)  // Swift should have a more accurate and helpful error message here now
}

обратите внимание, что я только показываю вам, что такое ошибка и как получить Xcode, чтобы дать вам лучшее сообщение об ошибке. То, что ты пытаешься вернуть, все еще неправильно.

только повторите попытку 401

Я не уверен, почему вы ожидаете, что этот код будет лечить 401 по-разному (кроме разноски в Центр уведомлений и ведения журнала). А так ты ловишь ... ошибка, но вы всегда возвращаете Observable С Error событие в конце (return Observable.error(error)), поэтому он никогда не повторит.

и 401 чтобы повторить попытку, вы должны вернуть Observable С retryWhen блок, который отправит Next событие (что означает, что вы хотите повторить). Для всех остальных кодов статуса, это Observable должны отправить Error (как вы сейчас делаете), что будет означать, что вы не хотите повторить, и что вы хотели бы ошибку пропагандируемый.

что-то вроде этого:

.retryWhen { errorObservable -> Observable<ErrorType> in
    log.debug("ReAuth error: \(error)")
    if case Error.StatusCode(let response) = error where response.statusCode == 401 {
        log.debug("401:, force user logout")
        NSNotificationCenter.defaultCenter().postNotificationName(Constants.Notifications.userNotAuthenticated, object: nil, userInfo: nil)
        // If `401`, then return the `Observable<ErrorType>` which was given to us
        // It will emit a `.Next<ErrorType>`
        // Since it is a `.Next` event, `retryWhen` will retry.
        return errorObservable
    }
    else {
        // If not `401`, then `flatMap` the `Observable<ErrorType>` which
        // is about to emit a `.Next<ErrorType>` into
        // an `Observable<ErrorType>` which will instead emit a `.Error<ErrorType>`.
        // Since it is an `.Error` event, `retryWhen` will *not* retry.
        // Instead, it will propagate the error.
        return errorObservable.flatMap { Observable.error() }
    }
}

когда вы catchError, Если это не ошибка 401, то вам просто нужно throw ошибка. Это отправит ошибку вниз по трубе.


я использовал этот код, он обновляет токен и повторно отправляет предыдущий запрос

static func send(request: TargetType) -> PrimitiveSequence<SingleTrait, Response> {
    return provider.rx.request(request)
        .retry(1)
        .observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default))
        .filterSuccessfulStatusAndRedirectCodes()
        .retryWhen({ (errorObservable: Observable<Error>) in
            errorObservable.flatMap({ (error) -> Single<String> in
                if let moyaError: MoyaError = error as? MoyaError, let response: Response = moyaError.response {
                        if **check invalid access token response codes here** {
                            return provider.rx.request(.refreshToken(*your refresh token here*))
                                .filterSuccessfulStatusCodes()
                                .mapString(atKeyPath: "*json path to new access token*")
                                .catchError { (_) in
                                    *Logout because getting new access token failed*
                                    throw error
                                }
                                .flatMap({ (newAccessToken) -> PrimitiveSequence<SingleTrait, String> in
                                    *set & save new access token*
                                    return Single.just(newAccessToken)
                                })
                        }
                }
                throw error
            })
        })
}

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

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

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