我正试图解决401错误场景,我想捕获错误,检查错误是否为401,如果是,
- 刷新oAuth
- 再次执行相同的API
目前,我正在做如下的事情:
return urlSession.dataTaskPublisher(for: request)
.tryMap(checkForAPIError)
.tryCatch { (error) -> AnyPublisher<(data: serverData, response: URLResponse), URLError> in
self.fetchoAuthToken()
.tryMap { (token) in
// Saves token
}
.receive(on: RunLoop.main)
.subscribe(on: DispatchQueue.main)
.sink { (completion) in
// Completion handling here
} receiveValue: { (value) in
print("Received (value)")
}
.store(in: &self.subscription)
return self.urlSession.dataTaskPublisher(for: request)
}
.tryMap(parseJson)
.retry(3)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
当前的问题对我来说是,而APIself.fetchoAuthToken()
仍在执行中,块返回新的请求。然后使用旧的令牌执行。
我希望self.fetchoAuthToken()
同步执行,以便在执行后可以返回,并且可以使用新的令牌。
如有任何帮助,不胜感激。
您需要链接发布者,并从tryCatch
返回链作为新发布者。
您通常应该避免副作用,但是如果您必须这样做——比如保存OAuth令牌,请在.handleEvents
中这样做,而不是创建sink
订阅。
return urlSession.dataTaskPublisher(for: request)
.tryMap(checkForAPIError)
.tryCatch { error in
self.fetchoAuthToken()
.handleEvents(receiveOutput: { (token) in
// Saves token
})
.flatMap { _ in
urlSession.dataTaskPublisher(for: request)
}
}
.tryMap(parseJson)
.retry(3)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
正如其他人所说,副作用(例如保存令牌)不应该成为处理管道的一部分(或至少不直接)。
建议通过添加一些helper功能将请求逻辑分成两部分:
/// this will do the actual URLSession call
/// fetchoAuthToken will likely call this with `withToken: false)
func _sendRequest(_ request: URLRequest, withToken: Bool = true) -> AnyPublisher<(data: Data, response: URLResponse), Error> {
let request = withToken ? addToken(to: request) : request
return urlSession
.dataTaskPublisher(for: request)
.mapError { $0 }
.eraseToAnyPublisher()
}
/// name explicit enough :)
func addToken(to request: URLRequest) -> URLRequest {
// do whathever needed (headers, cookies, etc)
}
enum MyError: Error {
/// `fetchoAuthToken` and `checkForAPIError` should return this
/// in case the request fails with a 401/403, or maybe some other
/// conditions
case notAuthenticated
}
现在,如果您将令牌获取和保存封装在fetchoAuthToken
中,并实现checkForAPIError
以在由于该原因导致请求失败的情况下返回MyError.notAuthenticated
,那么您最终可以得到一个漂亮的"纯净";管道:
return _sendRequest(request)
.tryMap(checkForAPIError)
.tryCatch { error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
if error as? MyError == .notAuthenticated {
return fetchToken().flatMap { _sendRequest(request) }.eraseToAnyPublisher()
} else {
throw error
}
}
.tryMap(parseJson)
.retry(3, unless: .isNotAuthenticated)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
您可能想要解决的另一个问题是retry
部分—如果fetchoAuthToken
由于无效凭据而失败,那么您可能希望跳过重试部分,因为如果凭据第一次无效,那么很可能它们第二次和第三次也会无效。
对于这个问题,你可以使用这个答案,并做一个retry unless
:
extension Error {
/// enabling sugar syntax for `retry`
var isNotAuthenticated: Bool { self as? MyError == .notAuthenticated }
}
return _sendRequest(request)
// rest of the pipeline omitted for readability
.retry(3, unless: .isNotAuthenticated)