跳到主要内容
教程目录

iOS 网络请求怎么调试?ATS 报错与 Windows 抓包 HTTPS 网络请求

最后更新

iOS 网络请求出问题,先看错误码:报 -1022 是 ATS 拦了 http 请求,优先把接口改成 https,改不了就只给那个域名配 NSExceptionDomains;抓包看不到 HTTPS 内容,多半是代理证书没在「证书信任设置」里开启完全信任,或者 App 自己做了证书锁定。ATS 和错误码以 Apple 官方文档和 iOS SDK 头文件为准,截至 2026 年 10 月。

下面依次讲怎么看懂 ATS 报错和 NSURLErrorDomain 错误码,在 Windows 或 Linux 上用抓包代理看 HTTPS 请求,在 iPhone 上安装并信任代理证书,再用 Logger 和 URLSessionTaskMetrics 在 App 里记录每个请求的耗时。

你需要一台已经能用 KXBuild 安装调试的 iPhone,步骤见设备准备。本文的抓包方法只用于调试你自己开发的 App。

iOS 网络请求失败先看哪个错误码?

URLSession 请求失败时,错误的 domain 是 NSURLErrorDomain,Swift 里是 URLError。先看错误码,再跳到对应的小节。下表的数值取自 iOS SDK 的 NSURLError.h。

错误码 Swift 名称 含义 先查什么
-999 .cancelled 请求被取消 代码里是否 cancel 了任务;证书锁定校验失败时也会走到取消
-1001 .timedOut 请求超时 服务器响应慢、代理没开或不通、timeoutInterval 太短
-1003 .cannotFindHost 域名解析不到 域名拼写、手机当前网络的 DNS、内网域名在外网解析不到
-1004 .cannotConnectToHost 连不上服务器端口 服务没启动、端口不对、防火墙拦截、填了代理但电脑上代理没运行
-1005 .networkConnectionLost 连接中途断开 服务器提前关闭连接、网络切换
-1009 .notConnectedToInternet 设备没有网络 飞行模式、蜂窝数据里关闭了这个 App 的网络权限
-1022 .appTransportSecurityRequiresSecureConnection 被 ATS 拦截 地址是 http,或服务器不满足 ATS 要求,见下一节
-1200 .secureConnectionFailed TLS 握手失败 服务器 TLS 版本过低、加密套件不符合 ATS,或代理证书问题
-1201 .serverCertificateHasBadDate 服务器证书已过期 续期服务器证书,顺便看看手机日期是否正确
-1202 .serverCertificateUntrusted 服务器证书不受信任 自签名证书、证书链不完整、抓包时代理证书没开完全信任
-1203 .serverCertificateHasUnknownRoot 证书的根证书未知 根证书不在系统信任列表里
-1204 .serverCertificateNotYetValid 证书尚未生效 证书生效时间晚于当前时间
-1206 .clientCertificateRequired 服务器要求客户端证书 服务端开了双向认证

在代码里拿到错误码的写法见后文的完整示例。

ATS 报错怎么解决?

报错原文与含义

App 传输安全(ATS)要求 URLSession 发出的 HTTP 请求都走 HTTPS,并在 TLS 的默认校验之外做额外检查。请求被拦截时,调试控制台会打印这段话,这是 Apple 文档里的原文。

1App Transport Security has blocked a cleartext HTTP (http://) resource
2load since it is insecure. Temporary exceptions can be configured via
3your app's Info.plist file.

代码里收到的错误是 NSURLErrorDomain 的 -1022,localizedDescription 是这一句。

1The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

除了 http 地址,服务器不满足下面任意一条,ATS 也会拦截。按 Apple 的《Preventing Insecure Network Connections》,ATS 的要求是:

  • TLS 1.2 或更高。
  • 证书用至少 2048 位的 RSA 密钥或至少 256 位的 ECC 密钥签名。
  • 证书使用 SHA-256 或更强的 SHA-2 摘要。
  • 使用 AES-128 或 AES-256 加密,支持 ECDHE 前向保密。

ATS 只管 URLSession 这一套 URL 加载系统。直接用 Network 框架或 CFNetwork 这类底层接口时,ATS 不介入,安全由你自己负责。

iOS 17 起 IP 地址也受限

开发时常见的情况是 App 请求电脑上的测试服务,例如 http://192.168.1.20:3000。按 Apple 的 NSAllowsLocalNetworking 文档,iOS 10 到 iOS 16 默认允许直接连 IP 地址、.local 域名和不带点的主机名。

iOS 17 起,ATS 默认不再允许连 IP 地址,要把 IP 地址或 CIDR 网段加进 NSExceptionDomains。所以同一个测试包,在 iOS 16 的手机上能连,在 iOS 17 以上就报 -1022。

例外的正确写法

先尽量把服务器改成 https,这是 Apple 文档的首选建议。改不了时,只给需要的域名开例外,不要整体关闭 ATS。

 1<key>NSAppTransportSecurity</key>
 2<dict>
 3  <key>NSExceptionDomains</key>
 4  <dict>
 5    <key>legacy.example.com</key>
 6    <dict>
 7      <key>NSIncludesSubdomains</key>
 8      <true/>
 9      <key>NSExceptionAllowsInsecureHTTPLoads</key>
10      <true/>
11    </dict>
12  </dict>
13</dict>

NSExceptionDomains 的域名按这几条规则写:

  • 用小写,例如 example.com。
  • 不带端口号,不写 example.com:443。
  • 不用通配符,不写 *.example.com,要包含子域名就设 NSIncludesSubdomains 为 YES。
  • iOS 17 起可以写 IPv4、IPv6 地址或 CIDR 网段,例如 192.168.42.63。
  • 例外按字面匹配。为域名开了例外,App 用 IP 访问同一台服务器时例外不生效,反过来也一样。

服务器只是 TLS 版本低,不需要 http,就用 NSExceptionMinimumTLSVersion 只降低这一项,例如设为 TLSv1.1,其他检查照常保留。

各种写法对比如下。

写法 影响范围 审核时
不加任何例外,服务器全用 https 无 不需要说明
NSExceptionDomains + NSExceptionAllowsInsecureHTTPLoads 只有列出的域名 需要说明理由
NSExceptionDomains + NSExceptionMinimumTLSVersion 只有列出的域名,只放宽 TLS 版本 需要说明理由
NSAllowsArbitraryLoadsInWebContent 只放开 WKWebView 里的网页 需要说明理由
NSAllowsArbitraryLoads 整个 App 的所有连接 需要说明理由,可能触发额外审核

Apple 文档写明,上面这几类例外提交 App Store 时都要提供理由,并且可能触发额外审核。文档举的可以考虑的理由包括:要连接的服务器属于别人、对方不支持安全连接;要连接无法升级的硬件设备。理由写在 App Store Connect 的审核备注里。

只在 Debug 包里放开测试服务器

连电脑上测试服务用的 IP 例外,不应该进正式包。做法是给 Debug 配置单独用一份 Info.plist。

  1. 复制一份 Info.plist 为 Info-Debug.plist,只在这份里加测试服务器的 IP 例外。
  2. 让 Debug 配置使用这份文件。
1kxbuild project add-setting -k INFOPLIST_FILE -v MyApp/Info-Debug.plist -t MyApp -c Debug -P .\MyApp.xcodeproj

路径按你工程里 Info.plist 的实际位置改。两份文件在 VS Code 里双击都能用属性列表编辑器编辑,其他键要保持一致。按配置区分设置的更多做法见 iOS 多环境打包。

怎么用抓包代理看 HTTPS 请求?

原理

抓包代理在电脑上运行,iPhone 把 Wi-Fi 的 HTTP 代理指向电脑,请求就都经过代理。HTTPS 请求要看到内容,代理要用自己的根证书临时签发服务器证书,所以手机上必须安装这个根证书并信任它。按 Apple 的 ATS 文档,手动安装并被用户信任的根证书可以作为信任锚点,所以不用改 App 的代码。

选一个代理工具

下面三个都能在 Windows 上用,mitmproxy 也能在 Linux 上用。只列官方文档里的通用设置。

工具 默认端口 允许手机连接 手机上下载证书
Fiddler Everywhere 8866 Settings → Connections → Allow remote devices to connect Safari 打开 http://电脑IP:8866,下载根证书
Charles 在 Proxy Settings 里查看 手机按 Charles 的端口设代理 Safari 打开 https://chls.pro/ssl
mitmproxy 8080 手机按电脑 IP 和端口设代理 Safari 打开 mitm.it,点 iOS 图标

Charles 还要在 Proxy Settings 的 SSL 页里,把要解密的主机名加进 SSL Proxying 列表,否则 HTTPS 请求只显示一条加密的连接。菜单名称随版本可能变化,以各工具官方文档中「iOS 设备抓包」和「证书」两部分为准。

Windows 防火墙默认会拦手机发来的连接。用管理员 PowerShell 放行代理端口,下面以 8866 为例。

1New-NetFirewallRule -DisplayName "Debug Proxy 8866" -Direction Inbound -Protocol TCP -LocalPort 8866 -Action Allow

查电脑的局域网 IP,Windows 上用 ipconfig,看当前网卡的 IPv4 地址;Linux 上用 ip addr。

iPhone 设置 Wi-Fi 手动代理

手机和电脑连同一个 Wi-Fi,然后:

  1. 打开「设置 → 无线局域网」,点已连接网络右侧的 ⓘ。
  2. 滑到底部,点「配置代理」,选「手动」。
  3. 服务器填电脑的 IP,端口填代理工具的端口,点「存储」。
  4. 用 Safari 打开任意一个 http 网页,代理工具里出现请求,说明代理通了。

Safari 打不开网页、代理工具里也没有请求,先查防火墙和 IP。这时你的 App 报的通常是 -1004 或 -1001。

安装证书并开启完全信任

  1. 在 iPhone 的 Safari 里打开上表中的证书地址,下载根证书,允许下载描述文件。
  2. 打开「设置」,按提示安装刚下载的描述文件。装好后可以在「设置 → 通用 → VPN 与设备管理」里看到它。
  3. 打开「设置 → 通用 → 关于本机 → 证书信任设置」,在「针对根证书启用完全信任」下打开刚装的证书。

第 3 步最容易漏。按 Apple 支持文档,手动安装的证书不会自动用于 SSL 信任,必须在这里手动开启;这一栏没出现,说明证书还没装上。漏了这一步,App 里的 HTTPS 请求会报 -1202 或 -1200,Safari 也会提示证书不受信任。

调试完恢复

调试结束马上做两件事,否则手机上所有 App 的流量都会继续经过代理,代理一关就全部断网。

  1. 「设置 → 无线局域网 → ⓘ → 配置代理」改回「关闭」。
  2. 在「设置 → 通用 → VPN 与设备管理」里删除代理的描述文件。

部分 Apple 自己的服务(例如 App Store)使用证书锁定,开着代理时可能无法使用,这是正常现象,关掉代理即可。Fiddler 的文档建议在代理工具里把 Apple 的域名加入不走代理的排除列表。

App 做了证书锁定怎么抓包?

证书锁定(certificate pinning)是 App 只接受特定的服务器证书或 CA。代理签发的证书不在名单里,请求就会失败。这是锁定在正常工作,不是代理的问题。

对你自己的 App,正确的做法是让锁定只在 Release 包里生效,Debug 包关掉,抓包就能照常进行。

  • 在 Info.plist 里用 NSPinnedDomains 锁定的,只写在 Release 用的 Info.plist 里,Debug 配置用上一节说的 Info-Debug.plist。
  • 在代码里锁定的,用编译条件做开关。Xcode 模板的 Debug 配置默认定义了 DEBUG。
 1import CryptoKit
 2import Foundation
 3import Security
 4
 5enum NetworkConfig {
 6    #if DEBUG
 7    static let pinningEnabled = false
 8    #else
 9    static let pinningEnabled = true
10    #endif
11
12    static let pinnedHost = "api.example.com"
13    static let pinnedCertificateHashes: Set<String> = [
14        "替换为你的服务器证书 SHA-256(Base64)"
15    ]
16}
17
18final class PinningDelegate: NSObject, URLSessionDelegate {
19    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async
20        -> (URLSession.AuthChallengeDisposition, URLCredential?) {
21        guard NetworkConfig.pinningEnabled,
22              challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
23              challenge.protectionSpace.host == NetworkConfig.pinnedHost,
24              let trust = challenge.protectionSpace.serverTrust else {
25            return (.performDefaultHandling, nil)
26        }
27        guard SecTrustEvaluateWithError(trust, nil),
28              let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
29              let leaf = chain.first else {
30            return (.cancelAuthenticationChallenge, nil)
31        }
32        let der = SecCertificateCopyData(leaf) as Data
33        let hash = Data(SHA256.hash(data: der)).base64EncodedString()
34        guard NetworkConfig.pinnedCertificateHashes.contains(hash) else {
35            return (.cancelAuthenticationChallenge, nil)
36        }
37        return (.useCredential, URLCredential(trust: trust))
38    }
39}

pinningEnabled 为 false 时返回 .performDefaultHandling,交给系统的默认校验,这时你在手机上信任的代理证书就能生效。锁定开启且证书不在名单里时,请求被取消,App 收到 -999。所以 Release 包抓不到包、报 -999,先确认是不是锁定在起作用。

第三方 SDK 自带锁定的,它的请求在代理下同样会失败,看 SDK 文档有没有调试开关。不要用任何方法去改动别人 App 的防护。

怎么在 App 里记录每个请求和耗时?

有些问题代理看不到,例如请求根本没发出去、DNS 慢、连接复用。这时在 App 里记录更直接。

用 Logger 打印请求,隐私脱敏

接口路径、状态码、耗时这类不敏感的内容标 privacy: .public;查询参数、请求头、请求体里可能有手机号和令牌,保持默认或标 .private,不要整个打出来。

1import Foundation
2import os
3
4extension Logger {
5    static let network = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.myapp", category: "network")
6}

不要打印 Authorization 请求头和完整的响应体。确实要看响应内容,只在 #if DEBUG 里临时打印,用完删掉。Logger 的级别和隐私选项见 iOS 真机日志怎么看。

用 URLSessionTaskMetrics 记录耗时

URLSessionTaskMetrics 记录一个任务从创建到完成的总时长和重定向次数,transactionMetrics 里每一项对应一次请求和响应,带 DNS、建连、TLS 握手、首字节、下载各阶段的起止时间。某个阶段没有发生(例如复用了连接),对应的时间就是 nil。

下面是一个完整的示例:用任务级的 delegate 收集耗时,同时把 URLError 的错误码翻译成中文写进日志。

 1import Foundation
 2import os
 3
 4final class RequestMetricsLogger: NSObject, URLSessionTaskDelegate {
 5    func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
 6        let path = task.originalRequest?.url?.path ?? "-"
 7        let totalMs = metrics.taskInterval.duration * 1000
 8        Logger.network.notice("\(path, privacy: .public) 总耗时 \(totalMs, format: .fixed(precision: 0)) ms,重定向 \(metrics.redirectCount) 次")
 9
10        for t in metrics.transactionMetrics {
11            let dns = Self.ms(t.domainLookupStartDate, t.domainLookupEndDate)
12            let connect = Self.ms(t.connectStartDate, t.connectEndDate)
13            let tls = Self.ms(t.secureConnectionStartDate, t.secureConnectionEndDate)
14            let firstByte = Self.ms(t.requestStartDate, t.responseStartDate)
15            let download = Self.ms(t.responseStartDate, t.responseEndDate)
16            let proto = t.networkProtocolName ?? "-"
17            Logger.network.notice("""
18            \(path, privacy: .public) \(proto, privacy: .public) 复用连接=\(t.isReusedConnection) 代理=\(t.isProxyConnection) \
19            DNS \(dns, privacy: .public) 连接 \(connect, privacy: .public) TLS \(tls, privacy: .public) \
20            首字节 \(firstByte, privacy: .public) 下载 \(download, privacy: .public)
21            """)
22        }
23    }
24
25    private static func ms(_ start: Date?, _ end: Date?) -> String {
26        guard let start, let end else { return "-" }
27        return String(format: "%.0fms", end.timeIntervalSince(start) * 1000)
28    }
29}
30
31struct APIClient {
32    let session: URLSession = .shared
33    let metricsLogger = RequestMetricsLogger()
34
35    func get(_ url: URL) async throws -> Data {
36        var request = URLRequest(url: url)
37        request.timeoutInterval = 15
38        Logger.network.info("GET \(url.path, privacy: .public) query=\(url.query ?? "", privacy: .private)")
39        do {
40            let (data, response) = try await session.data(for: request, delegate: metricsLogger)
41            let status = (response as? HTTPURLResponse)?.statusCode ?? -1
42            Logger.network.notice("GET \(url.path, privacy: .public) -> \(status) \(data.count) 字节")
43            return data
44        } catch let error as URLError {
45            Logger.network.error("GET \(url.path, privacy: .public) 失败 \(error.code.rawValue) \(Self.explain(error), privacy: .public)")
46            throw error
47        }
48    }
49
50    static func explain(_ error: URLError) -> String {
51        switch error.code {
52        case .appTransportSecurityRequiresSecureConnection: return "被 ATS 拦截,地址不是 https 或服务器不满足 ATS 要求"
53        case .timedOut: return "请求超时"
54        case .cannotFindHost: return "域名解析不到"
55        case .cannotConnectToHost: return "连不上服务器端口"
56        case .networkConnectionLost: return "连接中途断开"
57        case .notConnectedToInternet: return "设备没有网络"
58        case .secureConnectionFailed: return "TLS 握手失败"
59        case .serverCertificateHasBadDate: return "服务器证书已过期"
60        case .serverCertificateUntrusted: return "服务器证书不受信任"
61        case .serverCertificateHasUnknownRoot: return "服务器证书的根证书未知"
62        case .serverCertificateNotYetValid: return "服务器证书尚未生效"
63        case .cancelled: return "请求被取消"
64        default: return error.localizedDescription
65        }
66    }
67}

这段代码在 Swift 6 语言模式、iOS 16 部署目标下编译通过。data(for:delegate:) 从 iOS 15 起可用,传入的 delegate 只对这一个任务生效,不影响 URLSession.shared 的其他请求。

看日志时这样判断:

  • DNS 很长,是域名解析慢,换网络对比一下。
  • 连接和 TLS 都是 -,并且复用连接为 true,说明复用了已有连接,这是正常的。
  • 首字节很长、下载很短,时间花在服务器处理上,去查服务端。
  • 代理为 true,说明这个请求经过了代理。抓包时用它确认 App 的流量确实走了代理。

在 KXBuild Console 和 kxdevice 里看

  • 调试时。按 F5 运行后,Logger 的输出显示在 VS Code「输出 → KXBuild Console」。
  • 不调试时。从主屏幕打开 App,在电脑上按子系统过滤。
1kxdevice ostrace --subsystem com.example.myapp

只看出错的请求,加上级别过滤。

1kxdevice ostrace --subsystem com.example.myapp --levels error,fault

导出到文件、按分类过滤的方法见 iOS 真机日志怎么看。ATS 拦截那段英文提示在调试时出现在 KXBuild Console 里。

用 kxdevice 记录请求和网络包

不想改手机的代理设置时,KXBuild 还有两个命令可以用。

  • 记录 HTTP 请求为 HAR。先用 kxdevice apps ps --apps 找到 App 的 pid,再执行下面的命令,Ctrl-C 停止。导出的 .har 用浏览器开发者工具打开,看哪个接口最慢、有没有重复请求。
1kxdevice instruments har --pid 1234 -o .\myapp.har
  • 抓网络包。kxdevice pcap 把手机的网络包存成 libpcap 文件,可以按进程名过滤,用 Wireshark 打开。HTTPS 的内容是加密的,这种方式适合看连接有没有建立、连的是哪个 IP、TLS 握手在哪一步失败。
1kxdevice pcap .\myapp.pcap --proc MyApp

更多性能相关的命令见性能分析。

按错误码排查

现象 原因 处理
-1022,控制台提示 cleartext HTTP 被拦截 请求地址是 http 接口改 https;改不了的域名加 NSExceptionDomains
同一个测试包,iOS 16 能连电脑上的测试服务,iOS 17 以上报 -1022 iOS 17 起 ATS 默认不允许直接连 IP 地址 把 IP 加进 Debug 用的 Info.plist 的 NSExceptionDomains
加了 NSAllowsArbitraryLoads,某个域名还是被拦 这个域名在 NSExceptionDomains 里有单独的字典,全局键对它不生效 在这个域名的字典里加需要的例外
-1200,服务器是 https 服务器只支持 TLS 1.0 或 1.1,或加密套件不符合 ATS 升级服务器配置;临时用 NSExceptionMinimumTLSVersion
开代理后所有 HTTPS 请求报 -1202 或 -1200 代理证书没开完全信任 「设置 → 通用 → 关于本机 → 证书信任设置」里打开
开代理后请求报 -1004 或 -1001 电脑上代理没运行、IP 或端口填错、防火墙拦截 用 Safari 打开 http 网页验证代理是否通
证书信任了,自己的接口还是失败,报 -999 App 的证书锁定生效了 Debug 配置关闭锁定开关
-1003,内网域名在公司能用、在外面不能用 内网域名在外网解析不到 换成公网域名,或连回内网再测
-1009,手机上其他 App 能上网 这个 App 的网络权限被关掉了,例如在「设置 → 蜂窝网络」里关闭了它 在系统设置里给这个 App 打开网络权限
关掉代理后手机所有 App 都上不了网 Wi-Fi 的代理设置还指向电脑 「配置代理」改回「关闭」

表里没有的错误码,到 Foundation 的 NSURLError.h 头文件里按数值查对应的常量名,再用上面的方法确认 iOS 网络请求停在了哪一步。

常见问题

NSURLErrorDomain -1022 是什么意思?

-1022 是 NSURLErrorAppTransportSecurityRequiresSecureConnection,表示请求被 ATS 拦截,通常是地址用了 http。把接口改成 https;改不了时,只给那个域名在 Info.plist 的 NSExceptionDomains 里加 NSExceptionAllowsInsecureHTTPLoads。

NSAllowsArbitraryLoads 和 NSExceptionDomains 用哪个?

优先用 NSExceptionDomains,只放开需要的域名,App 的其他连接仍受 ATS 保护。NSAllowsArbitraryLoads 会关掉整个 App 的 ATS,按 Apple 文档两者提交审核时都要说明理由,整体关闭的理由更难写清。

抓包时 iPhone 上的 HTTPS 请求全部失败怎么办?

先确认代理的根证书已经安装,再到「设置 → 通用 → 关于本机 → 证书信任设置」里为它打开完全信任,这一步最容易漏。证书信任了只有自己的接口还失败,就检查 App 是否做了证书锁定,在 Debug 配置里关掉它。

为什么 iOS 17 上连不上电脑上的测试服务器?

按 Apple 文档,iOS 17 起 ATS 默认不再允许直接连接 IP 地址。把测试服务器的 IP 地址加进 NSExceptionDomains,建议只放在 Debug 配置用的 Info.plist 里,不要带进正式包。

不用抓包工具,怎么看 iPhone 上 App 的网络请求?

在 App 里用 Logger 打印请求路径、状态码和错误码,用 URLSessionTaskMetrics 记录各阶段耗时,调试时在 KXBuild Console 里看,不调试时用 kxdevice ostrace 按子系统过滤。也可以用 kxdevice instruments har 把请求记录成 HAR 文件。