URLSessionDataTaskPublisher错误处理异常及重复调用问题排查
runSyncTransactions重复调用的问题 问题背景
开发iOS应用时,通过远程API按周期获取交易数据,API限制每10秒仅可调用一次,IP封禁风险需规避。当网络返回HTTP错误(404、500等)或API返回预定义错误(认证令牌无效、日期错误等)时,需立即停止后续API调用队列。
当前实现存在问题:首次API调用出现不可达或预定义错误时,runSyncTransactions总会被调用两次。核心原因是网络请求为异步操作,代码未等待请求完成就检查lastApiError,此时lastApiError仍为nil,错误检查失效,导致循环继续执行下一次调用。
问题根源
runSyncTransactions作为异步函数,使用Combine的dataTaskPublisher发起网络请求,但未等待异步流完成就执行后续错误检查逻辑。网络请求的回调(错误或成功)会滞后于错误检查代码,导致lastApiError无法及时捕获错误,循环无法终止。
解决方案
将Combine异步流转换为async/await风格,让runSyncTransactions真正等待网络请求完成后再返回,确保错误能及时抛出并终止后续循环。
修改后的核心代码
private func runSyncTransactions(dateFrom: Date, dateTo: Date) async throws { if stopSync { stopSync = false throw SomeError.synchronizationStoppedByUser } let dateFromTimestamp = Int(dateFrom.timeIntervalSince1970) let dateToTimestamp = Int(dateTo.timeIntervalSince1970) guard let url = URL(string: "\(apiURLString)/\(dateFromTimestamp)/\(dateToTimestamp)") else { throw SomeError.error1 } var request = URLRequest(url: url) request.httpMethod = "GET" do { let (data, response) = try await URLSession.shared.data(for: request) // 校验HTTP响应状态码 guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { // 解析API自定义错误 if let apiError = try? JSONDecoder().decode(ApiErrorResponse.self, from: data) { let error = ApiError.makeError(errorResponse: apiError) print("Error receiving transactions: \(apiError.errorDescription)") throw error } else { throw SomeError.undefinedError } } // 解析成功返回的数据 let returnedItems = try JSONDecoder().decode([TransactionItem].self, from: data) print("Received transactions: \(returnedItems.count)") let transactionModelsTemp = returnedItems.map { TransactionConvertor.transactionItemToTransactonModel(item: $0) } // 回到主线程更新UI相关的Published属性 DispatchQueue.main.async { self.transactionModels = transactionModelsTemp } } catch { print("Error sync transactions: \(error.localizedDescription)") throw error } }
关键修改点
- 替换异步实现:移除Combine的
AnyCancellable订阅,改用URLSession.data(for:)的async/await版本,让函数等待网络请求完成后再执行后续逻辑。 - 简化错误处理:直接在
do/catch块中处理所有错误,无需依赖lastApiError变量,彻底避免异步竞态问题。 - 主线程更新UI:
async/await默认在后台线程执行,解析成功后通过DispatchQueue.main.async更新@Published属性,符合UI更新规则。 - 移除冗余变量:删除
lastApiError,通过直接抛出错误终止后续循环,逻辑更简洁可靠。
额外优化提示
原代码中dateTo参数每次都使用同一个固定日期(Calendar.current.date(byAdding: .day, value: -29, to: dateNow)!),建议修正为与dateFrom对应的区间(例如dateTo: Calendar.current.date(byAdding: .day, value: 29, to: date)),确保请求的日期范围符合业务需求。
内容的提问来源于stack exchange,提问作者Serhii R

