swift - 使用 RxSwift 和 URLSession 处理 401 状态

标签 swift rx-swift urlsession rx-cocoa

我目前有一个如下所示的网络客户端:

class Client<R: ResourceType> {

    let engine: ClientEngineType
    var session: URLSession

    init(engine: ClientEngineType = ClientEngine()) {
        self.engine = engine
        self.session = URLSession.shared
    }

    func request<T: Codable>(_ resource: R) -> Single<T> {
        let request = URLRequest(resource: resource)

        return Single<T>.create { [weak self] single in
            guard let self = self else { return Disposables.create() }
            let response = self.session.rx.response(request: request)

            return response.subscribe(
                onNext: { response, data in

                    if let error = self.error(from: response) {
                        single(.error(error))
                        return
                    }

                    do {
                        let decoder = JSONDecoder()
                        let value = try decoder.decode(T.self, from: data)
                        single(.success(value))
                    } catch let error {
                        single(.error(error))
                    }
            },
                onError: { error in
                    single(.error(error))
            })
        }
    }

    struct StatusCodeError: LocalizedError {
        let code: Int

        var errorDescription: String? {
            return "An error occurred communicating with the server. Please try again."
        }
    }

    private func error(from response: URLResponse?) -> Error? {
        guard let response = response as? HTTPURLResponse else { return nil }

        let statusCode = response.statusCode

        if 200..<300 ~= statusCode {
            return nil
        } else {
            return StatusCodeError(code: statusCode)
        }
    }
}

然后我可以调用类似的东西

let client = Client<MyRoutes>()
client.request(.companyProps(params: ["collections": "settings"]))
    .map { props -> CompanyModel in return props }
    .subscribe(onSuccess: { props in
      // do something with props
    }) { error in
        print(error.localizedDescription)
}.disposed(by: disposeBag)

我想开始处理 401 响应并刷新我的 token 并重试请求。

我正在努力寻找一种好方法来做到这一点。

我找到了this excellent gist它概述了实现这一目标的方法,但是我正在努力在当前的客户中实现这一点。

任何提示或指示将不胜感激。

最佳答案

这就是我的要点! (感谢您称赞它非常好。)您看过与之配套的文章吗? https://medium.com/@danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29

处理 401 重试有两个关键要素。首先,您需要一种将 token 插入到请求中并使用 Observable.deferred { tokenAcquisitionService.token.take(1) } 启动请求管道的方法。 。就您而言,这意味着您需要一个 URLRequest.init 来接受资源和 token ,而不仅仅是资源。

第二个是抛出 TokenAcquisitionError.unauthorized当您收到 401 并以 .retryWhen { $0.renewToken(with: tokenAcquisitionService) } 结束请求管道时出现错误

因此,鉴于您上面的内容,为了处理 token 重试,您需要做的就是将我的 TokenAcquisitionService 引入您的项目并使用它:

func getToken(_ oldToken: Token) -> Observable<(response: HTTPURLResponse, data: Data)> {
    fatalError("this function needs to be able to request a new token from the server. It has access to the old token if it needs that to request the new one.")
}

func extractToken(_ data: Data) -> Token {
    fatalError("This function needs to be able to extract the new token using the data returned from the previous function.")
}

let tokenAcquisitionService = TokenAcquisitionService<Token>(initialToken: Token(), getToken: getToken, extractToken: extractToken)

final class Client<R> where R: ResourceType {
    let session: URLSession

    init(session: URLSession = URLSession.shared) {
        self.session = session
    }

    func request<T>(_ resource: R) -> Single<T> where T: Decodable {
        return Observable.deferred { tokenAcquisitionService.token.take(1) }
            .map { token in URLRequest(resource: resource, token: token) }
            .flatMapLatest { [session] request in session.rx.response(request: request) }
            .do(onNext: { response, _ in
                if response.statusCode == 401 {
                    throw TokenAcquisitionError.unauthorized
                }
            })
            .map { (_, data) -> T in
                return try JSONDecoder().decode(T.self, from: data)
        }
        .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
        .asSingle()
    }
}

请注意,可能是 getToken例如,函数必须提供一个请求用户凭据的 View Controller 。这意味着您需要提供登录 View Controller (或 UIAlertController)来收集数据。或者,您可能在登录时从服务器获得授权 token 和刷新 token 。在这种情况下,TokenAcquisitionService 应该保留它们(即,它的 T 应该是 (token: String, refresh: String) 。两者都可以。

该服务的唯一问题是,如果获取新 token 失败,则整个服务将关闭。我还没解决这个问题。

关于swift - 使用 RxSwift 和 URLSession 处理 401 状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58120075/

相关文章:

ios - 使用其他图像和自定义颜色 mask 图像

swift - Rx swift : Observe viewDidLoad from view model without Subjects

swift - 为什么我们的 URLSession 对象是变量而不是常量

ios - 如何将下载的文件加载到 NSData 变量中

json - 如何在 Swift 中定义到同一模型对象的不同 JSON 映射?

ios - 传递滞后于一个 UIAction 按钮的值

ios - MovieViewController 的奇怪方向行为

swift - 如何制作可重置的 RxSwift 计时器?

swift - 创建包装 observable 的 Observable

swift - 下载音频文件