使用 Combine Publishers 的 401 重试机制
401 retry mechanism using Combine Publishers
Combine 还很陌生。
使用访问令牌和刷新令牌的常见场景。
您收到 401,您需要处理它(调用一些服务来刷新令牌),然后再次重试初始调用
func dataLoader(backendURL: URL) -> AnyPublisher<Data, Error> {
let request = URLRequest(url: backendURL)
return dataPublisher(for: request)
// We get here when a request fails
.tryCatch { (error) -> AnyPublisher<(data: Data, response: URLResponse), URLError> in
guard error.errorCode == 401 else { // UPS - Unauthorized request
throw error
}
// We need to refresh token and retry -> HOW?
// And try again
// return dataPublisher(for: request)
}
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw CustomError.invalidServerResponse
}
return data
}
.eraseToAnyPublisher()
}
我该如何包装这个 "token refresh service"?
您的代码提到了一个“令牌”,但您没有解释它是什么。假设您有一个令牌类型:
struct Token: RawRepresentable {
var rawValue: String
}
假设您有一个异步获取新令牌的函数,通过 return新令牌的发布者:
func freshToken() -> AnyPublisher<Token, Error> {
// Your code here, probably involving a URL request/response...
fatalError()
}
假设您通过将一些 URL 与令牌组合来生成 URL 数据请求:
func backendRequest(with url: URL, token: Token) -> URLRequest {
// Your code here, to somehow combine the url and the token into the real ...
fatalError()
}
现在您想重试请求,如果响应是 404,则每次都使用新的令牌。您可能应该限制尝试次数。因此,让我们编写函数来进行 triesLeft
计数。如果 triesLeft > 1
并且响应是 404,它将请求一个新的令牌并使用它再次调用自己(triesLeft
递减)。
目标变得更加复杂,因为 URLSession.DataTaskPublisher
不会将 404 响应变成错误。它将其视为正常输出。
所以我们将使用嵌套的辅助函数来处理 DataTaskPublisher
的输出,这样我们就不会在闭包中嵌套太多代码。名为 publisher(forDataTaskOutput:)
的辅助函数根据响应决定要做什么。
如果响应是代码为 200 的 HTTP 响应,它只是 return 数据。请注意,它必须 return 一个 Failure
为 Error
的发布者,因此它使用 Result.Pubilsher
并让 Swift 推导出 Failure
类型。
如果响应是代码为 404 的 HTTP 响应,并且 triesLeft > 1
,它会调用 freshToken
并使用 flatMap
将其链接到另一个调用外部函数。
否则,它会产生错误CustomError.invalidServerResponse
。
func data(atBackendURL url: URL, token: Token, triesLeft: Int) -> AnyPublisher<Data, Error> {
func publisher(forDataTaskOutput output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<Data, Error> {
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200):
return Result.success(output.data).publisher.eraseToAnyPublisher()
case .some(404) where triesLeft > 1:
return freshToken()
.flatMap { data(atBackendURL: url, token: [=13=], triesLeft: triesLeft - 1) }
.eraseToAnyPublisher()
default:
return Fail(error: CustomError.invalidServerResponse).eraseToAnyPublisher()
}
}
let request = backendRequest(with: url, token: token)
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { [=13=] as Error }
.flatMap(publisher(forDataTaskOutput:))
.eraseToAnyPublisher()
}
Combine 还很陌生。 使用访问令牌和刷新令牌的常见场景。
您收到 401,您需要处理它(调用一些服务来刷新令牌),然后再次重试初始调用
func dataLoader(backendURL: URL) -> AnyPublisher<Data, Error> {
let request = URLRequest(url: backendURL)
return dataPublisher(for: request)
// We get here when a request fails
.tryCatch { (error) -> AnyPublisher<(data: Data, response: URLResponse), URLError> in
guard error.errorCode == 401 else { // UPS - Unauthorized request
throw error
}
// We need to refresh token and retry -> HOW?
// And try again
// return dataPublisher(for: request)
}
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw CustomError.invalidServerResponse
}
return data
}
.eraseToAnyPublisher()
}
我该如何包装这个 "token refresh service"?
您的代码提到了一个“令牌”,但您没有解释它是什么。假设您有一个令牌类型:
struct Token: RawRepresentable {
var rawValue: String
}
假设您有一个异步获取新令牌的函数,通过 return新令牌的发布者:
func freshToken() -> AnyPublisher<Token, Error> {
// Your code here, probably involving a URL request/response...
fatalError()
}
假设您通过将一些 URL 与令牌组合来生成 URL 数据请求:
func backendRequest(with url: URL, token: Token) -> URLRequest {
// Your code here, to somehow combine the url and the token into the real ...
fatalError()
}
现在您想重试请求,如果响应是 404,则每次都使用新的令牌。您可能应该限制尝试次数。因此,让我们编写函数来进行 triesLeft
计数。如果 triesLeft > 1
并且响应是 404,它将请求一个新的令牌并使用它再次调用自己(triesLeft
递减)。
目标变得更加复杂,因为 URLSession.DataTaskPublisher
不会将 404 响应变成错误。它将其视为正常输出。
所以我们将使用嵌套的辅助函数来处理 DataTaskPublisher
的输出,这样我们就不会在闭包中嵌套太多代码。名为 publisher(forDataTaskOutput:)
的辅助函数根据响应决定要做什么。
如果响应是代码为 200 的 HTTP 响应,它只是 return 数据。请注意,它必须 return 一个
Failure
为Error
的发布者,因此它使用Result.Pubilsher
并让 Swift 推导出Failure
类型。如果响应是代码为 404 的 HTTP 响应,并且
triesLeft > 1
,它会调用freshToken
并使用flatMap
将其链接到另一个调用外部函数。否则,它会产生错误
CustomError.invalidServerResponse
。
func data(atBackendURL url: URL, token: Token, triesLeft: Int) -> AnyPublisher<Data, Error> {
func publisher(forDataTaskOutput output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<Data, Error> {
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200):
return Result.success(output.data).publisher.eraseToAnyPublisher()
case .some(404) where triesLeft > 1:
return freshToken()
.flatMap { data(atBackendURL: url, token: [=13=], triesLeft: triesLeft - 1) }
.eraseToAnyPublisher()
default:
return Fail(error: CustomError.invalidServerResponse).eraseToAnyPublisher()
}
}
let request = backendRequest(with: url, token: token)
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { [=13=] as Error }
.flatMap(publisher(forDataTaskOutput:))
.eraseToAnyPublisher()
}