使用 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 一个 FailureError 的发布者,因此它使用 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()
}