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()
}
在我们的分析中,我们可以清楚地看到事件链:
- 申请已更新
- 已打开申请
- 已显示主屏幕
- 显示警报 (NSURLErrorDomain -1)
- 应用后台
然后用户陷入循环“打开、警报、后台”,试图重新启动或重新安装应用程序但没有成功。
首先寻找的是它可能是从后端发送到客户端的一些垃圾,但我们的服务器日志有 api 调用的记录,按日期和时间与分析日志相关联,并带有 http 状态代码499.
所以我们可以明确的判断这不是服务器的问题。
在此更新之前,我们也没有用户的报告或分析记录。
所有指向新 API 客户端切换到 Combine。
看起来会话由于某种原因被客户端丢弃,但同时它与内存释放周期无关,因为 if cancellable where released sink
闭包将永远不会被执行,警报消息也不会显示。
问题:
- 此 URLSession 设置可能有什么问题?
- 您遇到过类似的行为并设法解决了吗?
- 您是否知道如何使用 URLSession 重现或至少模拟此类错误?
备注:
- 我们不使用 SwiftUI
- iOS 版本从 14.8 到 15.0
- 5% 到 10% 的用户受到影响
- 我们在开发或测试期间从未遇到过此类错误
我不确定,但我在您提供的代码中看到了几个问题...我在下面发表了评论。
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()
}
将 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()
}
在我们的分析中,我们可以清楚地看到事件链:
- 申请已更新
- 已打开申请
- 已显示主屏幕
- 显示警报 (NSURLErrorDomain -1)
- 应用后台 然后用户陷入循环“打开、警报、后台”,试图重新启动或重新安装应用程序但没有成功。
首先寻找的是它可能是从后端发送到客户端的一些垃圾,但我们的服务器日志有 api 调用的记录,按日期和时间与分析日志相关联,并带有 http 状态代码499.
所以我们可以明确的判断这不是服务器的问题。
在此更新之前,我们也没有用户的报告或分析记录。
所有指向新 API 客户端切换到 Combine。
看起来会话由于某种原因被客户端丢弃,但同时它与内存释放周期无关,因为 if cancellable where released sink
闭包将永远不会被执行,警报消息也不会显示。
问题:
- 此 URLSession 设置可能有什么问题?
- 您遇到过类似的行为并设法解决了吗?
- 您是否知道如何使用 URLSession 重现或至少模拟此类错误?
备注:
- 我们不使用 SwiftUI
- iOS 版本从 14.8 到 15.0
- 5% 到 10% 的用户受到影响
- 我们在开发或测试期间从未遇到过此类错误
我不确定,但我在您提供的代码中看到了几个问题...我在下面发表了评论。
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()
}