Swift中取消URLSession任务后重启请求时获取到nil数据的问题排查
问题分析与解决方案
首先看你遇到的NSURLErrorDomain Code=-999 "cancelled"错误,这说明第二次请求被主动取消了。追踪代码逻辑后,问题的根源很明确:
问题出在哪?
看IAPResponseCheck里这段代码:
IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in if isSuccessful == true { self.IAPTimer.invalidate() self.IAPTask?.cancel() // 这行代码是罪魁祸首! // ...其他弹窗逻辑 } }
再看IAPStatusCheck的实现,不管请求成功还是失败,它都会调用complition(true):
receiptValidation() { isPurchaseSchemeActive, error in if let err = error { self.onBuyProductHandler?(.failure(err)) } else { self.onBuyProductHandler?(.success(isPurchaseSchemeActive)) } complition(true) // 无论结果如何,都返回true }
这就导致:当你点击重试发起新请求后,只要receiptValidation的回调触发(哪怕是请求刚完成就出错),IAPResponseCheck就会立刻取消当前的IAPTask——也就是你刚发起的重试请求,所以才会拿到cancelled错误,并且data为nil。
修复步骤
1. 修正IAPStatusCheck的回调逻辑
只有当请求无错误且验证完成时才返回true,出错时返回false:
func IAPStatusCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom, complition: @escaping (_ isSuccessful: Bool)->()) { receiptValidation() { isPurchaseSchemeActive, error in if let err = error { self.onBuyProductHandler?(.failure(err)) // 区分主动取消的错误,避免误处理 if (err as NSError).code != NSURLErrorCancelled { // 可在这里处理其他非取消类错误 } complition(false) // 出错时返回false } else { self.onBuyProductHandler?(.success(isPurchaseSchemeActive)) complition(true) // 验证成功时返回true } } }
2. 移除IAPResponseCheck中不必要的取消逻辑
任务完成后会自动释放,不需要手动取消,只有超时场景才需要取消任务:
func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) { let infoDic: [String : String] = ["IAPReceiptValidationFrom" : iapReceiptValidationFrom.rawValue] self.IAPTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.IAPTimerAction), userInfo: infoDic, repeats: false) IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in if isSuccessful == true { self.IAPTimer.invalidate() // 移除这行错误的取消代码! // self.IAPTask?.cancel() self.getTopVisibleViewController { topViewController in if let viewController = topViewController { viewController.dismiss(animated: true, completion: nil) self.hideActivityIndicator() } } } else { // 请求失败时清理定时器 self.IAPTimer.invalidate() } } }
3. 额外优化:用原生超时替代Timer(可选)
其实URLRequest本身支持设置超时时间,比手动管理Timer更精准,还能减少代码复杂度:
var storeRequest = URLRequest(url: storeURL) storeRequest.httpMethod = "POST" storeRequest.httpBody = requestData storeRequest.timeoutInterval = 10.0 // 直接设置请求超时时间
之后在receiptValidation的回调里判断错误码:
if let err = error { if (err as NSError).code == NSURLErrorTimedOut { // 触发超时重试弹窗 self.showAlertForRetryIAP(...) } // ...其他错误处理 }
这样就可以完全去掉Timer相关的代码了。
验证修复效果
修改后,点击重试发起的新请求不会被无故取消,正常请求会走完验证流程,超时则会正确触发重试弹窗,整个流程就能正常工作了。
内容的提问来源于stack exchange,提问作者Tulon
相关产品推荐
相关产品推荐

