带有 DispatchGroup 的并行 URLSession 请求在 1 个请求中调用完成处理程序两次

Parallel URLSession requests w/ DispatchGroup call completion handler twice on 1 request

使用 DispatchGroup 我正在尝试 运行 2 个针对我的客户端的网络请求,并在两个都完成后返回结果。

我有一个问题,有时 DispatchGroup 请求之一的完成处理程序被调用两次,而另一个请求根本没有被调用。

一个例子是-


    func fetchProfileWithRelatedArticle(onSuccess: @escaping (User, [RelatedArticle]) -> Void, onError: @escaping (Error) -> Void) {
        let dispatchGroup = DispatchGroup()

        var user: User?
        var articles: [RelatedArticle] = []

        var errors: [Error] = []

        dispatchGroup.enter()
        fetchProfileForUser(onSuccess: {
            user = [=10=]
            print("fetchProfile:",[=10=])
            print("123")
            dispatchGroup.leave()
        }, onError: { error in
            errors.append(error)
            dispatchGroup.leave()
        })

        dispatchGroup.enter()
        getArticlesForUser(onSuccess: {
            articles = [=10=]
            print("getArticlesForUser:",[=10=])
            print("456")
            dispatchGroup.leave()
        }, onError: { error in
            errors.append(error)
            dispatchGroup.leave()
        })

        dispatchGroup.notify(queue: .main) {
            guard let user = user, errors.isEmpty else { return }

            onSuccess(user, articles)
        }
    }

在这里,我获取了用户个人资料,还获取了他们撰写的文章列表。这些通过完成处理程序返回并在其他地方显示。

大多数情况下这是可行的,但有时会出现这些请求中的任何一个会调用其自己的完成处理程序两次而另一个请求不会。

我怀疑这可能与我的访问令牌何时过期有关,因为如果我短时间离开该应用程序,就会发生这种情况。我的访问令牌的有效期为 2 分钟。

如果请求收到 401 响应,我的网络客户端中有以下方法请求新令牌,然后再次调用该调用。我相信这可能无法正常工作。


            if response.statusIs401() {
                self?.refreshHandler { success in
                    guard success else { completion(.failure(TokenError.refused)); return }
                    self?.request(resource, completion)
                }
                return
            }

我怀疑更新后再次调用该方法正在对我的调度组返回的请求进行处理。

是否可以以这种方式链接请求?


struct NoContent: Codable { }
typealias RefreshHandler = (@escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]

protocol ClientType: class {
    associatedtype Route: RouterType
    func request<T: Codable>(_ resource: Route, _ completion: @escaping  (Result<T>)-> Void)
}

class Client<Route: RouterType>: ClientType {

    enum APIError: Error {
        case unknown, badResponse, jsonDecoder, other
    }

    enum TokenError: String, Error {
        case expired = "Access Token Expired"
        case refused = "Refresh Token Failed"
    }

    private(set) var session: SessionType
    private(set) var tokenGetter: TokenGetter
    private(set) var refreshHandler: RefreshHandler

    private lazy var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
        return decoder
    }()

    init(session: SessionType, tokenGetter: @escaping TokenGetter, refreshHandler: @escaping RefreshHandler) {
        self.session = session
        self.tokenGetter = tokenGetter
        self.refreshHandler = refreshHandler
    }

    func request<T: Codable>(_ resource: Route, _ completion: @escaping  (Result<T>)-> Void) {

        let request = URLRequest(
            resource: resource,
            headers: tokenGetter()
        )

        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard error == nil else { completion(.failure(APIError.unknown)); return }
            guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }

            if response.statusIs401() {
                self?.refreshHandler { success in
                    guard success else { completion(.failure(TokenError.refused)); return }
                    self?.request(resource, completion)
                }
                return
            }

            if response.statusIsSuccess() {
                guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
                do {
                    let value = try self.decoder.decode(T.self, from: data)
                    DispatchQueue.main.async {
                        completion(.success(value))
                    }
                } catch let error {
                    print(error)
                }
                return
            }

            completion(.failure(APIError.other))
        }.resume()
    }

    // some calls return a 200/201 with no data
    private func deserializeNoContentResponse(data: Data?) -> Data? {

        if data?.count == 0 {
            return "{ }".data(using: .utf8)
        }

        return data
    }
}

听起来您需要在网络客户端中执行以下操作:

func makeTheRequest(_ completion: CompletionHandler) {
  URLSession.shared.dataTask(with: someURL) { data, response, error in 
    guard let httpResponse = response as? HTTPURLResponse else {
      return
    }

    if httpResponse.statusCode == 401 {
      self.refreshToken { success in
        if success { self.makeTheRequest(completion) }
      }
    }

    // handle response
    completion(whateverDataYouNeedToPass)
  }
}

这将进行调用,检查响应代码,刷新需要的令牌,如果成功,它会调用应该再次使用完成处理程序进行响应的方法,该处理程序首先传递给它而不调用它第一的。因此,直到第二次调用 API 之后才会调用完成处理程序。

当然,自己的代码采用这个,应该不会太难做