UIKit下Combine实现URLSession请求出现NSURLErrorDomain -1错误求助
将API客户端切换为Combine实现后,陆续收到用户反馈遇到The operation couldn’t be completed (NSURLErrorDomain -1.)错误,该错误是API客户端直接将error.localizedDescription透传到UI层展示导致的。
上层API调用代码如下:
class SomeViewModel { private let serviceCategories: ICategoriesService private var cancellables = [AnyCancellable]() init(service: ICategoriesService) { self.serviceCategories = service } // ... // 这段代码确实很糙,我们目前还在往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 // 省略业务逻辑 // 出错时展示弹窗 } .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 // 这里把错误映射后给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,可排除服务端问题,且本次版本更新前没有同类用户反馈。所有线索都指向新的Combine实现的API客户端:看起来请求是客户端主动断开的,但该问题和内存释放逻辑无关,因为如果cancellable被释放,sink闭包不会执行,也就不会弹出错误提示。
备注信息
- 未使用SwiftUI
- 受影响用户的iOS版本在14.8到15.0之间
- 受影响用户占比为5%~10%
- 开发测试阶段从未复现过该错误
1. URLSession相关实现存在的问题
核心问题是iOS 14.5~15.0版本存在Combine系统Bug:对URLSession.DataTaskPublisher手动调用subscribe(on:)指定调度器时,会导致内部网络任务被意外取消,客户端主动断开连接,服务端就会返回499状态码,取消对应的错误码就是NSURLErrorDomain -1(对应URLError.unknown)。
你这里的dataTaskPublisher本身就默认在后台队列执行网络请求,完全不需要额外调用subscribe(on: DispatchQueue.background),这行多余的代码正好触发了系统版本Bug。
另外错误映射逻辑直接透传了系统错误的localizedDescription到UI层,才会把原始的系统报错直接展示给用户。
2. 同类问题的解决方法
直接删掉dataTaskPublisher后面的.subscribe(on: DispatchQueue.background)这行代码即可解决绝大部分问题,后续的tryMap、decode等操作会默认继承上游的后台调度器执行,如果你需要指定后续回调的调度器,用receive(on:)指定即可,不要对URLSession.DataTaskPublisher使用subscribe(on:)。
如果需要兼容更多场景,可以额外加2~3次自动重试逻辑,针对NSURLErrorDomain -1的错误做降级处理,不要直接透传系统错误描述给用户。
如果要彻底规避该版本的Combine坑,也可以自行封装URLSessionDataTask为自定义Publisher,避免使用系统自带的dataTaskPublisher。
3. 复现该错误的方法
- 准备iOS 14.8或者15.0版本的测试设备/模拟器,在调用
dataTaskPublisher时保留subscribe(on:)的代码,同时并发发起多个网络请求(比如你代码里用combineLatest同时发起两个请求的场景),大概率能复现该问题。 - 可以自定义
URLProtocol拦截网络请求,手动抛出URLError(.unknown),就能模拟出完全一致的报错信息。 - 配合Network Link Conditioner工具调整为高丢包弱网环境,同时发起多个并发请求,也能提升复现概率。
内容的提问来源于stack exchange,提问作者George

