StoreKit 购买触发 NSURLErrorDomain -1001、-1003 和 -1004
结论
这些错误来自 NSURLErrorDomain,属于网络错误,并非 StoreKit 私有错误码:
| 错误码 | 常量 | 含义 |
|---|---|---|
-1001 | NSURLErrorTimedOut | 请求超时 |
-1003 | NSURLErrorCannotFindHost | 无法解析或找到服务器主机 |
-1004 | NSURLErrorCannotConnectToHost | 找到了主机,但无法建立连接 |
出现这些错误,通常说明设备与 App Store 或 Sandbox 服务通信失败。代码签名、provisioning profile 和产品配置一般不会直接导致这几个错误。
错误码有明确含义,但仅凭它们无法断定 Sandbox 出现故障。DNS、网络路由、VPN、代理、设备状态或 Apple 服务端异常,都可能产生相同的错误。
addPayment: 本身不会同步返回错误
[[SKPaymentQueue defaultQueue] addPayment:aPayment] 的返回类型是 void。调用后,StoreKit 会异步处理交易,并通过 SKPaymentTransactionObserver 报告状态。
实际的失败信息通常出现在:
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
失败交易的 transaction.error 可能属于 SKErrorDomain,底层网络错误则可能位于 NSUnderlyingErrorKey 中。排查时需要同时记录外层错误和底层错误,不能只看 error.code。
- (void)logStoreError:(NSError *)error {
NSError *currentError = error;
while (currentError != nil) {
NSLog(@"domain=%@ code=%ld description=%@ userInfo=%@",
currentError.domain,
(long)currentError.code,
currentError.localizedDescription,
currentError.userInfo);
currentError = currentError.userInfo[NSUnderlyingErrorKey];
}
}
交易监听可以这样处理:
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
// 完成收据验证和内容发放后再结束交易。
[queue finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self logStoreError:transaction.error];
if (![transaction.error.domain isEqualToString:SKErrorDomain] ||
transaction.error.code != SKErrorPaymentCancelled) {
// 提示用户当前购买失败,并允许稍后重试。
}
[queue finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
// 恢复内容后结束交易。
[queue finishTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing:
case SKPaymentTransactionStateDeferred:
// 仍在处理或等待批准,不要结束,也不要重复提交。
break;
}
}
}
应用启动后应尽早注册交易观察者,不要等到用户点击购买时才注册:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
为什么产品请求成功,购买仍然会失败
SKProductsRequest 成功,说明设备当时能够获取商品元数据,传入的产品标识符也能被识别,但这不能证明后续交易服务可用。
购买过程还可能涉及:
- App Store 账户或 Sandbox 测试账户认证;
- 与交易服务器建立新的连接;
- 处理现有的未完成交易;
- 创建订单并更新交易队列;
- 家庭或监护审批;
- 收据生成和更新。
商品查询和支付交易使用的目标服务、连接时机与状态要求可能不同。即使商品请求成功,后续购买仍可能因 DNS、连接超时或服务端暂时不可用而失败。
商品信息也可能来自缓存,所以商品列表能够显示,并不能证明交易服务器此时可以访问。
建议的排查顺序
1. 检查完整错误链
先确认外层错误的 domain 和 code,再沿 NSUnderlyingErrorKey 查找底层错误。
可以用下面的辅助函数判断错误链中是否包含这三类网络错误:
static BOOL IsTransientStoreNetworkError(NSError *error) {
NSError *currentError = error;
while (currentError != nil) {
if ([currentError.domain isEqualToString:NSURLErrorDomain]) {
switch (currentError.code) {
case NSURLErrorTimedOut:
case NSURLErrorCannotFindHost:
case NSURLErrorCannotConnectToHost:
return YES;
default:
break;
}
}
currentError = currentError.userInfo[NSUnderlyingErrorKey];
}
return NO;
}
记录错误时不要只保留数字。缺少错误域,单看 -1001 之类的数字无法确定其含义。
2. 按暂时性网络故障处理
遇到 -1001、-1003 或 -1004 时,可以保留用户当前页面,显示清楚的失败提示,并允许用户稍后重试。
不要在后台反复调用 addPayment:。自动重试可能产生重复的购买请求,也可能掩盖仍处于 Purchasing 或 Deferred 状态的交易。只有交易明确进入 Failed 状态后,才允许用户重新发起购买。
3. 检查设备和网络差异
如果问题只出现在一台设备上,可以检查:
- 系统日期、时间和时区是否正确,最好启用自动设置;
- 是否启用了 VPN、HTTP 代理、DNS 过滤或内容过滤软件;
- 当前网络是否限制 Apple 服务;
- App Store 或 Sandbox 测试账户是否可用;
- 队列中是否有尚未处理的旧交易;
- 重启设备后问题是否仍然存在;
- 切换蜂窝网络和 Wi-Fi 后,错误是否随网络变化。
清理项目、重新安装 Xcode、重新生成证书或 provisioning profile,通常无法解决 NSURLErrorDomain 网络错误。
4. 判断 Sandbox 是否可能出现故障
如果多台测试设备在不同网络上同时出现相同错误,而生产环境或其他互联网请求正常,那么 Sandbox 服务异常的可能性较大。
这只是排除本地原因后得出的判断。-1001、-1003 和 -1004 无法单独证明 Apple Sandbox 已经宕机。应记录错误发生时间、设备系统版本、网络环境、外层 SKError 和完整的底层错误,供后续复现或提交反馈。
交易处理中的注意事项
- 不要对处于
SKPaymentTransactionStatePurchasing或SKPaymentTransactionStateDeferred状态的交易调用finishTransaction:。 - 已购买的交易应在收据验证、发放状态保存和内容交付全部完成后再结束。
- 失败交易可以在记录错误后结束。用户主动取消购买时,不应显示网络故障提示。
- 商品请求成功不能用于判断购买服务是否正常。
- 对于消耗型商品,服务端发放逻辑应具备幂等性,避免重试或恢复流程造成重复发放。
- Sandbox 可能出现间歇性波动。应用应把网络失败作为正常的运行时情况处理,使用户能够恢复或重试。
备注:内容仅供参考。
