将我们的API客户端切换到Combine后,我们开始收到用户关于错误的报告" The operation cannot be complete (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%的用户受影响
- 我们从未遇到过这样的错误在开发或测试
我不确定,但我在您提供的代码中看到了几个问题…
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()
}