使用 URLSession 和 Combine 处理 HTTP 状态码

handling HTTP status code with URLSession and Combine

我正在尝试处理来自 DataTaskPublisher 读取其响应状态代码的响应。

当状态代码大于 299 时,我想 return 一个 ServiceError 类型为失败。在我见过的每个示例中,我都使用了 .mapError.catch... 在这种特定情况下,来自 .flatMap,我真的不知道如何处理发布者响应 return 错误而不是 TResponse...

    return URLSession.DataTaskPublisher(request: urlRequest, session: .shared)
        .mapError{error in return ServiceError.request}
        .flatMap{ data, response -> AnyPublisher<TResponse, ServiceError> in

            if let httpResponse = response as? HTTPURLResponse,
                (200...299).contains(httpResponse.statusCode){

                return Just(data)
                    .decode(type: TResponse.self, decoder: JSONDecoder())
                    .mapError{error in return ServiceError.decode}
                    .eraseToAnyPublisher()
            }else{
                //???? HOW TO HANDLE THE ERROR?
            }
        }
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()

如果我正确理解了你的目标,你需要像

这样的东西
}else{
    return Fail(error: ServiceError.badServiceReply)
              .eraseToAnyPublisher()
}

简单示例:

URLSession.shared
    .dataTaskPublisher(for: URL(string: "https://www.google.com")!)
    .receive(on: DispatchQueue.main)
    .flatMap { _ in
        Fail(error: URLError(URLError.unsupportedURL)).eraseToAnyPublisher()
    } //for the sake of the demo
    .replaceError(with: "An error occurred") //this sets Failure to Never
    .assign(to: \.stringValue, on: self)
    .store(in: &cancellableBag)

将始终分配字符串 "An error occurred",因为重新映射到 Fail 发布者

enum ServiceErrors: Error { 
    case internalError(_ statusCode: Int)
    case serverError(_ statusCode: Int)
}
    
return URLSession.shared.dataTaskPublisher(for: urlRequest)
            .tryMap { data, response in
                guard let httpResponse = response as? HTTPURLResponse,
                    200..<300 ~= httpResponse.statusCode else {
                        switch (response as! HTTPURLResponse).statusCode {
                        case (400...499):
                            throw ServiceErrors.internalError((response as! HTTPURLResponse).statusCode)
                        default:
                            throw ServiceErrors.serverError((response as! HTTPURLResponse).statusCode)
                        }
                }
                return data
            }
            .mapError { [=10=] as! ServiceErrors }
            .decode(type: T.self, decoder: JSONDecoder())
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()

注意: 我依靠 this link 来制作我的错误处理程序。

您可以在此处使用 tryMap

URLSession.shared.dataTaskPublisher(for: urlRequest)
    .mapError { error in return ServiceError.request }
    .tryMap { (data, response) throws -> TResponse in
        guard let httpResponse = response as? HTTPURLResponse,
              200...299 ~= httpResponse.statusCode else {
                  throw ServiceError.invalidStatusCode((response as? HTTPURLResponse)?.statusCode ?? 0)
              }
        return try JSONDecoder().decode(TResponse.self, from: data)
    }
    .receive(on: RunLoop.main)
    .eraseToAnyPublisher()

这也将简化您的代码,因为您不需要创建额外的发布者。

我还对原始代码进行了其他一些改进:

  • 使用 URLSession.shared.dataTaskPublisher(for: urlRequest) 而不是 URLSession.DataTaskPublisher(request: urlRequest, session: .shared)
  • 使用~=运算符检查状态码
  • 使用 guard 而不是 let,因为它更惯用