【问题标题】:Swift Combine in UIKit. URLSession dataTaskPublisher NSURLErrorDomain -1 for some usersUIKit 中的 Swift 组合。某些用户的 URLSession dataTaskPublisher NSURLErrorDomain -1
【发布时间】:2021-12-11 23:31:13
【问题描述】:

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

顶级 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 { $0(.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()
}

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

  • 应用程序已更新
  • 应用程序已打开
  • 显示主屏幕
  • 显示警报 (NSURLErrorDomain -1)
  • 应用程序后台 然后用户陷入循环“打开,警报,后台”尝试重新启动或重新安装应用程序但没有成功。

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

所有指向新 API 客户端的点都切换到了 Combine。

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

问题:

  • 此 URLSession 设置有什么问题?
  • 您是否遇到过类似的行为并设法解决?
  • 您知道如何使用 URLSession 重现或至少模拟此类错误吗?

注意事项:

  • 我们不使用 SwiftUI
  • iOS 版本从 14.8 到 15.0 不等
  • 5% 到 10% 的用户受到影响
  • 我们在开发或测试期间从未遇到过此类错误

【问题讨论】:

  • 您有分析功能,显然有问题的设备能够访问互联网 - 但如果它无法访问您的特定 URL,该怎么办?您的分析是否与您尝试访问的 URL 位于同一端点?我在公司环境中工作,我们的网络将允许我们访问某些 URL,但不能访问其他 URL。也许用户处于分析可以通过的环境中,但是这些请求试图访问的任何 URL 都被阻止。所以会话失败。
  • DispatchQueue.background是什么类型的DispatchQueue?
  • @LuLuGaGa DisoatchQueue.background 只是 DisoatchQueue 扩展中的 static let background = DispatchQueue.global(qos: .background)
  • @ScottThompson 你没记错 - 分析是第三方的,它可以工作。但与此同时,我们在服务器上记录了准确的 api 调用,因此客户端显然可以到达我们的服务器,但由于某种原因,它在服务器生成响应时中途断开连接。大部分请求在 2 到 200 毫秒之间变化,没有一个超过 3 秒,因此没有理由超时,因为我们使用 URLSession.shared,默认超时为 60 秒。

标签: ios swift combine urlsession nsurlsessiondatatask


【解决方案1】:

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

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()
}

【讨论】:

  • 我曾尝试在使用演示服务器完成请求之前删除可取消项,但无法重现任何错误。请求只是永远挂起,接收器关闭从未执行,而在现实生活中的示例用户观察带有错误消息的警报。但在演示环境中,可能有一些我没有做出正确的警告。
  • 根据我对发布者的理解(确实非常原始),我认为subscribe(on) 应该将所有下游调度程序设置为指定线程,这样我们就可以在后台线程中执行解析、映射和其他操作,直到将结果呈现给 UI 的最后一个接收器。原来我错了。我会删除这个电话,如果错误消失,我会回来接受你的回答。谢谢。
  • 您描述的是receive(on:) 方法,而不是subscribe(on:)receive(on:) 方法设置下游调度程序。但即使在这种情况下也不需要这样做,因为dataTaskPublisher 已经在后台线程上发出。
猜你喜欢
  • 1970-01-01
  • 2020-12-25
  • 1970-01-01
  • 1970-01-01
  • 2019-11-07
  • 2017-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多