Swift使用URLSession请求偶现NSURLErrorDomain -1005连接丢失错误如何解决
错误原因分析
NSURLErrorDomain Code=-1005 对应的官方错误类型是 NSURLErrorNetworkConnectionLost,核心触发逻辑是:客户端使用已经被服务端/中间代理节点主动关闭的HTTP Keep-Alive长连接发送请求,系统检测到连接已经失效就会抛出该错误。你在重试场景下高频触发该错误,是因为刷新Token后立刻复用了刚结束请求的旧连接,此时旧连接刚好被服务端回收,导致请求失败。
现有代码的问题
- 无重试次数限制:
retry()方法是递归调用,如果持续返回触发刷新的状态码,会进入无限重试循环,短时间大量请求会触发服务端限流、主动断开连接,进一步放大-1005错误的出现概率。 - Token刷新逻辑存在并发冲突:多个请求同时触发Token刷新时,会同时发起多个刷新请求,服务端通常刷新一次就会作废旧的Refresh Token,导致后续刷新请求全部失败,引发更多重试请求形成风暴。
- 错误处理存在漏判:
refreshAccessToken方法中guard let data = data, err == nil else {return}分支直接退出,没有调用回调函数,外层请求逻辑会一直挂起,请求堆积会引发连接资源耗尽、连接异常。 - 解析逻辑存在崩溃风险:
do-catch块内使用try!解析JSON,解析失败会直接触发崩溃,完全绕过了catch逻辑。 - performRetry变量无线程安全保障:如果该变量是全局/类属性,多线程并发读写时会出现状态错乱,导致重复重试、逻辑异常。
原生URLSession解决方案
1. 优化Token刷新逻辑,增加并发排队机制
同一时间仅允许一个刷新请求执行,其余刷新请求排队等待结果,避免重复发起刷新请求:
// 新增类属性控制刷新并发 private var isRefreshing = false private var pendingRefreshCallbacks: [(Bool, Bool, String) -> Void] = [] private let refreshLock = NSLock() func refreshAccessToken(completionHandler: @escaping (Bool, Bool, String) -> Void) { refreshLock.lock() // 已有刷新请求在执行,将当前回调加入排队队列 if isRefreshing { pendingRefreshCallbacks.append(completionHandler) refreshLock.unlock() return } isRefreshing = true refreshLock.unlock() URLSession.shared.dataTask(with: tokenRegenerateRequest) { data, res, err in var isSuccess = false var isFailed = false var errCode = "" defer { // 刷新完成后执行所有排队回调 refreshLock.lock() self.isRefreshing = false let callbacks = self.pendingRefreshCallbacks self.pendingRefreshCallbacks.removeAll() refreshLock.unlock() callbacks.forEach { $0(isSuccess, isFailed, errCode) } } print("ERROR: ", err) print("DATA: ", data) if let _ = err { print("IN HERE") isFailed = true errCode = "INTERNAL_SERVER_ERROR" completionHandler(isSuccess, isFailed, errCode) return } guard let data = data, let httpResponse = res as? HTTPURLResponse else { isFailed = true errCode = "INVALID_RESPONSE" completionHandler(isSuccess, isFailed, errCode) return } if httpResponse.statusCode == 200, !data.isEmpty { do { // 替换try!为正常try,避免崩溃 let decodedResponse = try JSONDecoder().decode(RefreshTokenResponse.self, from: data) print("ACCESS TOKEN REGENERATE REQUEST: ", decodedResponse) if !decodedResponse.accessToken.isEmpty && !decodedResponse.refreshToken.isEmpty { // 此处需要将新Token更新到全局请求头配置中 isSuccess = true completionHandler(isSuccess, isFailed, errCode) } else { print("HERE1") isFailed = true completionHandler(isSuccess, isFailed, errCode) } } catch { print("HERE2: \(error)") isFailed = true completionHandler(isSuccess, isFailed, errCode) } } else { print("HERE4", httpResponse.statusCode) isFailed = true errCode = "\(httpResponse.statusCode)" completionHandler(isSuccess, isFailed, errCode) } }.resume() }
2. 优化重试逻辑,增加次数限制和延迟
避免无限重试,同时增加短延迟等待系统清理失效连接,避免复用旧连接:
// retry方法新增重试次数字段,默认从0开始计数 func retry(urlString: String, method: String, requestBody: [String: Any], completionHandler: @escaping (Any?, HTTPURLResponse?, String?) -> Void, retryCount: Int = 0) { // 最多重试3次,避免无限循环 guard retryCount < 3 else { completionHandler(nil, nil, "MAX_RETRY_EXCEEDED") return } // 执行原请求逻辑 sendOriginalRequest(urlString: urlString, method: method, requestBody: requestBody) { [weak self] data, response, error in guard let self = self else { return } if let responseDesc = response?.allHeaderFields["responseDesc"] as? String, responseDesc == "SOMECODE" { var performRetry = false self.refreshAccessToken() { isSuccess,isFailed, errCode in if isSuccess { performRetry = true // 增加200ms延迟后重试,等待系统清理失效连接 DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { self.retry(urlString: urlString, method: method, requestBody: requestBody, completionHandler: completionHandler, retryCount: retryCount + 1) } } else { if !performRetry { if errCode.isEmpty { completionHandler(nil,nil,"OHNO") } else { completionHandler(nil,nil,"ERROR") } } } } } else { completionHandler(data, response, error) } } }
3. 可选优化:自定义URLSession配置
如果要进一步降低连接复用失效的概率,可以不用系统默认的URLSession.shared,自定义Session调整连接参数:
private lazy var customSession: URLSession = { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 // 调整单域名最大连接数,根据业务实际情况调整 config.httpMaximumConnectionsPerHost = 10 return URLSession(configuration: config) }() // 后续所有网络请求统一使用customSession发起即可
内容的提问来源于stack exchange,提问作者SHA
相关产品推荐
相关产品推荐

