使用Combine Publishers的401重试机制

Giz*_*Jon 4 mobile networking ios swift 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()
}
Run Code Online (Sandbox Code Playgroud)

我将如何包装这个“令牌刷新服务”?

rob*_*off 9

您的代码提到了“令牌”,但您没有解释那是什么。假设您有一个令牌类型:

struct Token: RawRepresentable {
    var rawValue: String
}
Run Code Online (Sandbox Code Playgroud)

假设您有一个函数通过返回新令牌的发布者异步获取新令牌:

func freshToken() -> AnyPublisher<Token, Error> {
    // Your code here, probably involving a URL request/response...
    fatalError()
}
Run Code Online (Sandbox Code Playgroud)

假设您通过将一些 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()
}
Run Code Online (Sandbox Code Playgroud)

现在您想重试请求,每次使用新令牌,如果响应是 404。您可能应该限制尝试次数。因此,让我们编写函数来进行triesLeft计数。如果triesLeft > 1响应是 404,它将要求一个新的令牌并使用它再次调用自己(triesLeft递减)。

目标变得更加复杂,因为URLSession.DataTaskPublisher不会将 404 响应变成错误。它将其视为正常输出。

所以我们将使用嵌套的辅助函数来处理 的输出DataTaskPublisher,这样我们就不会在闭包中嵌套太多代码。名为 的辅助函数publisher(forDataTaskOutput:)根据响应决定要执行的操作。

  • 如果响应是代码为 200 的 HTTP 响应,则它只返回数据。请注意,它必须返回一个发布者,其Failureis Error,因此它使用 aResult.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: $0, 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 { $0 as Error }
        .flatMap(publisher(forDataTaskOutput:))
        .eraseToAnyPublisher()
}
Run Code Online (Sandbox Code Playgroud)

  • 这里要检查的错误代码是 401 而不是 404,并且在 freshToken 函数返回 401 的情况下不应重试请求 (2认同)