简体   繁体   中英

Alamofire/RxSwift how to refresh token and retry requests automatically on status code 401

I need help with automatically retrying requests after i get first 401 status code on any request. I'm using RxSwift and Alamofire so the call looks like this:

public func getSomeEndpointInfo() -> Observable<PKKInfo> {
    return Observable.create({ observer in
        let request = AF.request(Router.info)
        request
            .responseDecodable(of: Info.self) { response in
                print("response: \(response)")
                if response.response?.statusCode == 401 {
                    observer.onError(NetworkError.unauthorized)
                }
                guard let decodedItems = response.value else {
                    observer.onError(NetworkError.invalidJSON)
                    return
                }
                observer.onNext(decodedItems)
                observer.onCompleted()
            }
        return Disposables.create()
    })
}

Now in some service I have the following code:

service.getSomeEndpointInfo()
.observe(on: MainScheduler.instance)
.subscribe { [unowned self] info in
    self._state.accept(.loaded)
} onError: { [unowned self] error in
    print("---> Error")
    self.sessionManager
        .renewToken()
        .observe(on: MainScheduler.instance)
        .subscribe { token in
            print("---> recieved new token")
            self.service.getSomeEndpointInfo()
        } onError: { error in
            print("---> error generating token")
        }.disposed(by: self.disposeBag)
}.disposed(by: disposeBag)

With this code works but I have to call renew token on every request and its embedded into error subscription which doesn't feel well. If you have some other suggestion that on 401 I somehow retry requests and trigger renew token before that i would be grateful.

I wrote an article on how to do this. RxSwift and Handling Invalid Tokens .

The article comes complete with code and tests proving functionality. The key is the class at the bottom of this answer.

You use it like this:

typealias Response = (URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)>

func getData<T>(response: @escaping Response, tokenAcquisitionService: TokenAcquisitionService<T>, request: @escaping (T) throws -> URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
    return Observable
        .deferred { tokenAcquisitionService.token.take(1) }
        .map { try request($0) }
        .flatMap { response($0) }
        .map { response in
            guard response.response.statusCode != 401 else { throw TokenAcquisitionError.unauthorized }
            return response
        }
        .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
}

You can use currying to make a function that shares the service...

func makeRequest(builder: @escaping (MyTokenType) -> URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
    getData(
        response: { URLSession.shared.rx.response(request: $0) /* or however Moya makes network requests */ },
        tokenAcquisitionService: TokenAcquisitionService<MyTokenType>(
            initialToken: getSavedToken(),
            getToken: makeRenewTokenRequest(oldToken:),
            extractToken: extractTokenFromData(_:)),
        request: builder)
}

Use the above function anywhere in your code that needs token renewal.

Here is the TokenAquisitionService used above. Have all your requests use the same service object.


public final class TokenAcquisitionService<T> {

    /// responds with the current token immediatly and emits a new token whenver a new one is aquired. You can, for example, subscribe to it in order to save the token as it's updated.
    public var token: Observable<T> { get }

    public typealias GetToken = (T) -> Observable<(response: HTTPURLResponse, data: Data)>

    /// Creates a `TokenAcquisitionService` object that will store the most recent authorization token acquired and will acquire new ones as needed.
    ///
    /// - Parameters:
    ///   - initialToken: The token the service should start with. Provide a token from storage or an empty string (object represting a missing token) if one has not been aquired yet.
    ///   - getToken: A function responsable for aquiring new tokens when needed.
    ///   - extractToken: A function that can extract a token from the data returned by `getToken`.
    public init(initialToken: T, getToken: @escaping GetToken, extractToken: @escaping (Data) throws -> T)

    /// Allows the token to be set imperativly if necessary.
    /// - Parameter token: The new token the service should use. It will immediatly be emitted to any subscribers to the service.
    public func setToken(_ token: T)
}

extension ObservableConvertibleType where Element == Error {
    /// Monitors self for `.unauthorized` error events and passes all other errors on. When an `.unauthorized` error is seen, the `service` will get a new token and emit a signal that it's safe to retry the request.
    ///
    /// - Parameter service: A `TokenAcquisitionService` object that is being used to store the auth token for the request.
    /// - Returns: A trigger that will emit when it's safe to retry the request.
    public func renewToken<T>(with service: TokenAcquisitionService<T>) -> Observable<Void>
}

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