使用 RxSwift 和 URLSession 处理 401 状态
Handling 401 status w/ RxSwift & URLSession
我目前有一个如下所示的网络客户端:
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
响应并刷新我的令牌并重试请求。
我正在努力寻找一个好的方法来做到这一点。
我发现 this excellent gist 概述了一种实现此目的的方法,但我正在努力在我当前的客户端中实现它。
任何提示或指示将不胜感激。
这就是我的要点! (感谢您称它为优秀。)您看到它附带的文章了吗? https://medium.com/@danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
处理401重试有两个关键要素。首先是您需要一种方法将令牌插入您的请求并使用 Observable.deferred { tokenAcquisitionService.token.take(1) }
启动您的请求管道。在您的情况下,这意味着您需要一个 URLRequest.init 来接受资源和令牌,而不仅仅是资源。
第二个是在收到 401 时抛出 TokenAcquisitionError.unauthorized
错误并以 .retryWhen { [=13=].renewToken(with: tokenAcquisitionService) }
结束请求管道
因此,根据以上内容,为了处理令牌重试,您需要做的就是将我的 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 { [=10=].renewToken(with: tokenAcquisitionService) }
.asSingle()
}
}
请注意,getToken
函数可能必须,例如,呈现一个请求用户凭据的视图控制器。这意味着您需要提供您的登录视图控制器(或 UIAlertController)来收集数据。或者,当您登录时,您可能会从服务器获得授权令牌和刷新令牌。在那种情况下,TokenAcquisitionService 应该同时保留它们(即,它的 T
应该是 (token: String, refresh: String)
。两者都可以。
该服务唯一的问题是,如果获取新令牌失败,整个服务将关闭。我还没修好。
我目前有一个如下所示的网络客户端:
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
响应并刷新我的令牌并重试请求。
我正在努力寻找一个好的方法来做到这一点。
我发现 this excellent gist 概述了一种实现此目的的方法,但我正在努力在我当前的客户端中实现它。
任何提示或指示将不胜感激。
这就是我的要点! (感谢您称它为优秀。)您看到它附带的文章了吗? https://medium.com/@danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
处理401重试有两个关键要素。首先是您需要一种方法将令牌插入您的请求并使用 Observable.deferred { tokenAcquisitionService.token.take(1) }
启动您的请求管道。在您的情况下,这意味着您需要一个 URLRequest.init 来接受资源和令牌,而不仅仅是资源。
第二个是在收到 401 时抛出 TokenAcquisitionError.unauthorized
错误并以 .retryWhen { [=13=].renewToken(with: tokenAcquisitionService) }
因此,根据以上内容,为了处理令牌重试,您需要做的就是将我的 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 { [=10=].renewToken(with: tokenAcquisitionService) }
.asSingle()
}
}
请注意,getToken
函数可能必须,例如,呈现一个请求用户凭据的视图控制器。这意味着您需要提供您的登录视图控制器(或 UIAlertController)来收集数据。或者,当您登录时,您可能会从服务器获得授权令牌和刷新令牌。在那种情况下,TokenAcquisitionService 应该同时保留它们(即,它的 T
应该是 (token: String, refresh: String)
。两者都可以。
该服务唯一的问题是,如果获取新令牌失败,整个服务将关闭。我还没修好。