Swift 在 UIKit 中合并。某些用户的 URLSession dataTaskPublisher NSURLErrorDomain -1

Swift Combine in UIKit. URLSession dataTaskPublisher NSURLErrorDomain -1 for some users

将 API 客户端切换到 Combine 后,我们开始收到用户关于错误“操作无法完成 (NSURLErrorDomain -1.)”的报告,这是 error.localizedDescription 转发的从我们的 API 客户到 UI。

顶级 api 调用如下所示:

class SomeViewModel {
  private let serviceCategories: ICategoriesService
  private var cancellables = [AnyCancellable]()

  init(service: ICategoriesService) {
    self.serviceCategories = service
  }

  // ...

  // Yes, the block is ugly. We are only on the half way of the migration to Combine
  func syncData(force: Bool = false, _ block: @escaping VoidBlock) {
    serviceCategories
      .fetch(force: force)
      .combineLatest(syncOrders(ignoreCache: force))
      .receive(on: DispatchQueue.main)
      .sink { [unowned self] completion in
        // bla-bla-bla
        // show alert on error
      }
      .store(in: &cancellables)
  }
}

低级别 API 客户端调用如下所示:

func fetch<R>(_ type: R.Type, at endpoint: Endpoint, page: Int, force: Bool) -> AnyPublisher<R, TheError> where R : Decodable {
  guard let request = request(for: endpoint, page: page, force: force) else {
    return Deferred { Future { [=12=](.failure(TheError.Network.cantEncodeParameters)) } }.eraseToAnyPublisher()
  }

  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase

  return URLSession.shared
      .dataTaskPublisher(for: request)
      .subscribe(on: DispatchQueue.background)
      .tryMap { element in
        guard
          let httpResponse = element.response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else
        { throw URLError(.badServerResponse) }
        
        return element.data
      }
      .decode(type: type, decoder: decoder)
      .mapError { error in
        // We map error to present in UI
        switch error {
        case is Swift.DecodingError:
          return TheError.Network.cantDecodeResponse
          
        default:
          return TheError(title: nil, description: error.localizedDescription, status: -2)
        }
      }
      .eraseToAnyPublisher()
}

在我们的分析中,我们可以清楚地看到事件链:

首先寻找的是它可能是从后端发送到客户端的一些垃圾,但我们的服务器日志有 api 调用的记录,按日期和时间与分析日志相关联,并带有 http 状态代码499.
所以我们可以明确的判断这不是服务器的问题。 在此更新之前,我们也没有用户的报告或分析记录。

所有指向新 API 客户端切换到 Combine。

看起来会话由于某种原因被客户端丢弃,但同时它与内存释放周期无关,因为 if cancellable where released sink 闭包将永远不会被执行,警报消息也不会显示。

问题:

备注:

我不确定,但我在您提供的代码中看到了几个问题...我在下面发表了评论。

A 499 表示您的 Cancellable 在网络请求完成之前被删除。也许这会帮助您找到它。

此外,您不需要 subscribe(on:),而且它可能不会像您认为的那样工作。这可能是导致问题的原因,但无法确定。

使用 subscribe(on:) 就像这样做:

DispatchQueue.background.async {
    URLSession.shared.dataTask(with: request) { data, response, error in
        <#code#>
    }
}

如果您了解 URLSession 的工作原理,您会发现分派是完全没有必要的,并且不会影响数据任务将在哪个线程上发出。

func fetch<R>(_ type: R.Type, at endpoint: Endpoint, page: Int, force: Bool) -> AnyPublisher<R, TheError> where R : Decodable {
    guard let request = request(for: endpoint, page: page, force: force) else {
        return Fail(error: TheError.Network.cantEncodeParameters).eraseToAnyPublisher() // your failure here is way more complex than it needs to be. A simple Fail will do what you need here.
    }

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    return URLSession.shared
        .dataTaskPublisher(for: request)
        // you don't need the `subscribe(on:)` here.
        .tryMap { element in
            guard
                let httpResponse = element.response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else
                { throw URLError(.badServerResponse) }

            return element.data
        }
        .decode(type: type, decoder: decoder)
        .mapError { error in
            // We map error to present in UI
            switch error {
            case is Swift.DecodingError:
                return TheError.Network.cantDecodeResponse

            default:
                return TheError(title: nil, description: error.localizedDescription, status: -2)
            }
        }
        .eraseToAnyPublisher()
}