iOS Network Debugging: ATS Errors and HTTPS Proxy on Windows
Last updated
iOS network debugging starts with the error code: -1022 means ATS blocked an http request, so move the endpoint to https first, and only if you can’t, add an NSExceptionDomains entry for that one domain. If the proxy can’t show HTTPS content, the certificate usually hasn’t been given full trust in Certificate Trust Settings, or the app pins its certificates. ATS behavior and error codes follow Apple’s documentation and the iOS SDK headers as of October 2026.
This tutorial covers how to read ATS errors and NSURLErrorDomain error codes, inspect HTTPS requests with a debugging proxy on Windows or Linux, install and trust the proxy certificate on your iPhone, and log the timing of every request inside the app with Logger and URLSessionTaskMetrics.
You need an iPhone that you can already install and debug on with KXBuild. See Device setup. Use the proxy techniques here only to debug apps you develop yourself.
Which error code should iOS network debugging start with?
When a URLSession request fails, the error domain is NSURLErrorDomain, which is URLError in Swift. Check the code first, then jump to the matching section. The values below come from NSURLError.h in the iOS SDK.
| Code | Swift name | Meaning | Check first |
|---|---|---|---|
| -999 | .cancelled |
The request was cancelled | Whether your code cancelled the task. A failed certificate pinning check also ends up here |
| -1001 | .timedOut |
The request timed out | A slow server, a proxy that’s off or unreachable, a timeoutInterval that’s too short |
| -1003 | .cannotFindHost |
The host name couldn’t be resolved | Spelling, the DNS of the phone’s current network, internal domains that don’t resolve outside your network |
| -1004 | .cannotConnectToHost |
Couldn’t connect to the server port | The service isn’t running, the wrong port, a firewall, or a proxy is configured but not running on the computer |
| -1005 | .networkConnectionLost |
The connection dropped midway | The server closed the connection early, or the network changed |
| -1009 | .notConnectedToInternet |
The device has no network | Airplane Mode, or network access for this app turned off under cellular data |
| -1022 | .appTransportSecurityRequiresSecureConnection |
Blocked by ATS | The URL is http, or the server doesn’t meet ATS requirements. See the next section |
| -1200 | .secureConnectionFailed |
The TLS handshake failed | An old TLS version on the server, cipher suites that don’t meet ATS, or a proxy certificate problem |
| -1201 | .serverCertificateHasBadDate |
The server certificate has expired | Renew the server certificate, and check that the phone’s date is right |
| -1202 | .serverCertificateUntrusted |
The server certificate isn’t trusted | A self-signed certificate, an incomplete chain, or a proxy certificate without full trust |
| -1203 | .serverCertificateHasUnknownRoot |
The certificate’s root is unknown | The root certificate isn’t in the system trust list |
| -1204 | .serverCertificateNotYetValid |
The certificate isn’t valid yet | Its start date is later than the current time |
| -1206 | .clientCertificateRequired |
The server requires a client certificate | The server uses mutual TLS |
To get the error code in your code, see the complete example below.
How do you fix ATS errors?
The message and what it means
App Transport Security (ATS) requires HTTP requests made with URLSession to use HTTPS, and adds checks on top of default TLS validation. When a request is blocked, the debug console prints this message, quoted from Apple’s documentation.
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.
The error your code receives is -1022 in NSURLErrorDomain, and its localizedDescription is this.
1The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
Besides http URLs, ATS also blocks servers that fail any of the requirements below. According to Apple’s Preventing Insecure Network Connections, ATS requires the following.
- TLS 1.2 or later.
- A certificate signed with an RSA key of at least 2048 bits or an ECC key of at least 256 bits.
- A certificate hash of SHA-256 or a stronger SHA-2 digest.
- AES-128 or AES-256 encryption with ECDHE forward secrecy.
ATS only covers the URL Loading System that URLSession belongs to. When you use lower-level APIs such as the Network framework or CFNetwork directly, ATS doesn’t apply and security is up to you.
IP addresses are restricted from iOS 17
During development, an app often calls a test service on your computer, such as http://192.168.1.20:3000. According to Apple’s NSAllowsLocalNetworking documentation, iOS 10 through iOS 16 allow connections to IP addresses, .local domains and unqualified host names by default. From iOS 17, ATS no longer allows IP addresses by default, and you have to add the IP address or CIDR range to NSExceptionDomains. That’s why the same test build connects on an iOS 16 phone and fails with -1022 on iOS 17 and later.
Write exceptions correctly
Move the server to https if you can, which is the first recommendation in Apple’s documentation. If you can’t, add an exception only for the domains that need it, and don’t turn off ATS entirely.
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>
Write domains in NSExceptionDomains according to these rules.
- Use lowercase, such as
example.com. - Leave out the port. Don’t write
example.com:443. - Don’t use wildcards such as
*.example.com. To include subdomains, setNSIncludesSubdomainsto YES. - From iOS 17 you can use IPv4 and IPv6 addresses or CIDR ranges, such as
192.168.42.63. - Exceptions match literally. An exception for a domain doesn’t apply when the app reaches the same server by IP, and the other way around.
If the server only has an old TLS version and doesn’t need http, use NSExceptionMinimumTLSVersion to relax just that, for example to TLSv1.1, and keep every other check.
Here’s how the options compare.
| Option | Scope | At App Review |
|---|---|---|
| No exceptions, all servers on https | None | No justification needed |
NSExceptionDomains with NSExceptionAllowsInsecureHTTPLoads |
Only the listed domains | Justification required |
NSExceptionDomains with NSExceptionMinimumTLSVersion |
Only the listed domains, only the TLS version is relaxed | Justification required |
NSAllowsArbitraryLoadsInWebContent |
Only web content in WKWebView | Justification required |
NSAllowsArbitraryLoads |
Every connection in the app | Justification required, may trigger additional review |
Apple’s documentation states that these exceptions need a justification when you submit to the App Store and may trigger additional review. Examples it gives of reasons you might use include connecting to a server owned by someone else that doesn’t support secure connections, and connecting to a hardware device that can’t be upgraded. Put the justification in the review notes in App Store Connect.
Allow the test server only in Debug builds
IP exceptions for a test service on your computer shouldn’t ship in release builds. Give the Debug configuration its own Info.plist.
- Copy
Info.plisttoInfo-Debug.plist, and add the test server’s IP exception only in the copy. - Point the Debug configuration at that file.
1kxbuild project add-setting -k INFOPLIST_FILE -v MyApp/Info-Debug.plist -t MyApp -c Debug -P .\MyApp.xcodeproj
Adjust the path to where your project’s Info.plist actually is. Double-click either file in VS Code to edit it in the Property List Editor, and keep the other keys identical. For more ways to vary settings per configuration, see iOS Ad Hoc and App Store builds from one project with xcconfig.
How do you inspect HTTPS requests with a proxy?
How it works
The proxy runs on your computer. When you point the iPhone’s Wi-Fi HTTP proxy at the computer, every request goes through the proxy. To see HTTPS content, the proxy issues server certificates on the fly with its own root certificate, so the phone has to install that root certificate and trust it. According to Apple’s ATS documentation, a root certificate the user installs and trusts manually can act as a trust anchor, so you don’t need to change the app’s code.
Choose a proxy tool
All three below work on Windows, and mitmproxy also works on Linux. Only the general settings from their official docs are listed.
| Tool | Default port | Allow the phone to connect | Download the certificate on the phone |
|---|---|---|---|
| Fiddler Everywhere | 8866 | Settings > Connections > Allow remote devices to connect | Open http://<computer IP>:8866 in Safari and download the root certificate |
| Charles | Shown in Proxy Settings | Point the phone’s proxy at Charles’s port | Open https://chls.pro/ssl in Safari |
| mitmproxy | 8080 | Point the phone’s proxy at the computer’s IP and port | Open mitm.it in Safari and tap the iOS icon |
In Charles, also add the host names you want to decrypt to the SSL Proxying list on the SSL tab of Proxy Settings, or HTTPS requests show up only as an encrypted connection. Menu names may change between versions, so follow the iOS device and certificate sections of each tool’s official docs.
Windows Firewall blocks connections from the phone by default. Allow the proxy port from an administrator PowerShell. The example uses 8866.
1New-NetFirewallRule -DisplayName "Debug Proxy 8866" -Direction Inbound -Protocol TCP -LocalPort 8866 -Action Allow
To find the computer’s LAN IP, run ipconfig on Windows and look at the IPv4 address of the active adapter, or run ip addr on Linux.
Set a manual Wi-Fi proxy on the iPhone
Connect the phone and the computer to the same Wi-Fi network, then do the following.
- Open Settings > Wi-Fi and tap ⓘ next to the connected network.
- Scroll to the bottom, tap Configure Proxy and choose Manual.
- Enter the computer’s IP as the server and the proxy tool’s port, then tap Save.
- Open any http page in Safari. If the request shows up in the proxy tool, the proxy works.
If Safari can’t load the page and nothing shows up in the proxy, check the firewall and the IP first. In that state your app usually reports -1004 or -1001.
Install the certificate and enable full trust
- In Safari on the iPhone, open the certificate URL from the table above, download the root certificate and allow the profile download.
- Open Settings and install the downloaded profile when prompted. Once installed, it appears under Settings > General > VPN & Device Management.
- Open Settings > General > About > Certificate Trust Settings and turn on the certificate under Enable Full Trust for Root Certificates.
Step 3 is the one people miss most. According to Apple Support, a manually installed certificate isn’t trusted for SSL automatically, and you have to turn it on here. If the section doesn’t appear, the certificate isn’t installed yet. Without this step, HTTPS requests in the app fail with -1202 or -1200, and Safari also warns that the certificate isn’t trusted.
Clean up after debugging
When you’re done, do two things right away. Otherwise all traffic from every app on the phone keeps going through the proxy, and everything loses network access as soon as the proxy stops.
- Set Settings > Wi-Fi > ⓘ > Configure Proxy back to Off.
- Delete the proxy’s profile in Settings > General > VPN & Device Management.
Some of Apple’s own services, such as the App Store, use certificate pinning and may not work while the proxy is on. That’s expected, and turning off the proxy fixes it. Fiddler’s docs suggest adding Apple’s domains to the proxy tool’s exclusion list.
How do you debug an app that pins certificates?
With certificate pinning, an app accepts only specific server certificates or CAs. The certificate issued by the proxy isn’t on that list, so the request fails. That’s pinning doing its job, not a proxy problem.
For your own app, the right approach is to enforce pinning only in Release builds and turn it off in Debug builds, so the proxy works as usual.
- If you pin in Info.plist with NSPinnedDomains, put it only in the Info.plist used by Release, and have the Debug configuration use the
Info-Debug.plistfrom the previous section. - If you pin in code, use a compilation condition as the switch. The Debug configuration in Xcode templates defines
DEBUGby default.
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 "Replace with your server certificate's 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}
When pinningEnabled is false, the delegate returns .performDefaultHandling and leaves validation to the system, so the proxy certificate you trusted on the phone takes effect. When pinning is on and the certificate isn’t on the list, the request is cancelled and the app gets -999. So if you can’t capture a Release build’s traffic and see -999, check whether pinning is the cause first.
If a third-party SDK pins its own certificates, its requests also fail through the proxy. Check the SDK’s docs for a debug switch. Don’t tamper with the protections in apps that aren’t yours.
How do you log each request and its timing in the app?
Some problems never show up in a proxy, such as requests that are never sent, slow DNS or connection reuse. Logging inside the app is more direct for these.
Log requests with Logger and redact private data
Mark non-sensitive values such as the path, status code and timing as privacy: .public. Query parameters, headers and bodies can contain phone numbers and tokens, so leave them at the default or mark them .private, and don’t log them in full.
1import Foundation
2import os
3
4extension Logger {
5 static let network = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.myapp", category: "network")
6}
Don’t log the Authorization header or full response bodies. If you really need to see a response, print it temporarily inside #if DEBUG and remove it afterwards. For Logger levels and privacy options, see Read iPhone device logs with Logger and os_log, then filter and export them.
Record timing with URLSessionTaskMetrics
URLSessionTaskMetrics records a task’s total duration from creation to completion and its redirect count. Each entry in transactionMetrics is one request and response, with start and end times for DNS, connecting, the TLS handshake, the first byte and the download. If a phase didn’t happen, for example because a connection was reused, its times are nil.
Here is a complete example. A task-level delegate collects timing, and URLError codes are turned into readable explanations in the log.
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) total \(totalMs, format: .fixed(precision: 0)) ms, \(metrics.redirectCount) redirects")
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) reused=\(t.isReusedConnection) proxy=\(t.isProxyConnection) \
19 DNS \(dns, privacy: .public) connect \(connect, privacy: .public) TLS \(tls, privacy: .public) \
20 firstByte \(firstByte, privacy: .public) download \(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) bytes")
43 return data
44 } catch let error as URLError {
45 Logger.network.error("GET \(url.path, privacy: .public) failed \(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 "Blocked by ATS, the URL isn't https or the server doesn't meet ATS requirements"
53 case .timedOut: return "The request timed out"
54 case .cannotFindHost: return "The host name couldn't be resolved"
55 case .cannotConnectToHost: return "Couldn't connect to the server port"
56 case .networkConnectionLost: return "The connection dropped midway"
57 case .notConnectedToInternet: return "The device has no network"
58 case .secureConnectionFailed: return "The TLS handshake failed"
59 case .serverCertificateHasBadDate: return "The server certificate has expired"
60 case .serverCertificateUntrusted: return "The server certificate isn't trusted"
61 case .serverCertificateHasUnknownRoot: return "The server certificate's root is unknown"
62 case .serverCertificateNotYetValid: return "The server certificate isn't valid yet"
63 case .cancelled: return "The request was cancelled"
64 default: return error.localizedDescription
65 }
66 }
67}
This code compiles in the Swift 6 language mode with an iOS 16 deployment target. data(for:delegate:) is available from iOS 15, and the delegate you pass applies only to that one task, without affecting other requests on URLSession.shared.
Read the log like this.
- Long DNS means slow name resolution. Compare on another network.
- Connect and TLS both
-with reused=true means an existing connection was reused, which is normal. - Long first byte with a short download means the time went into server processing, so look at the server.
- proxy=true means the request went through a proxy. Use it while capturing to confirm the app’s traffic really goes through the proxy.
View logs in KXBuild Console and kxdevice
- While debugging. After you press F5, Logger output appears in VS Code under Output > KXBuild Console.
- Without debugging. Open the app from the Home Screen and filter by subsystem on your computer.
1kxdevice ostrace --subsystem com.example.myapp
To see only failed requests, add a level filter.
1kxdevice ostrace --subsystem com.example.myapp --levels error,fault
To export to a file or filter by category, see Read iPhone device logs with Logger and os_log, then filter and export them. The ATS block message appears in KXBuild Console while you debug.
Record requests and packets with kxdevice
If you’d rather not change the phone’s proxy settings, KXBuild has two more commands.
- Record HTTP requests as HAR. Find the app’s pid with
kxdevice apps ps --apps, then run the command below and press Ctrl-C to stop. Open the .har file in your browser’s developer tools to see which endpoint is slowest and whether any requests repeat.
1kxdevice instruments har --pid 1234 -o .\myapp.har
- Capture network packets.
kxdevice pcapsaves the phone’s network packets to a libpcap file, optionally filtered by process name, which you open in Wireshark. HTTPS content stays encrypted, so this is for seeing whether a connection was established, which IP it went to and at which step the TLS handshake failed.
1kxdevice pcap .\myapp.pcap --proc MyApp
For more performance commands, see Profiling.
Troubleshoot by error code
| Symptom | Cause | Fix |
|---|---|---|
| -1022, and the console says a cleartext HTTP load was blocked | The URL is http | Move the endpoint to https. For domains you can’t change, add NSExceptionDomains |
| The same test build reaches the test service on your computer on iOS 16 but fails with -1022 on iOS 17 and later | From iOS 17, ATS doesn’t allow IP addresses by default | Add the IP to NSExceptionDomains in the Debug Info.plist |
NSAllowsArbitraryLoads is set but one domain is still blocked |
That domain has its own dictionary in NSExceptionDomains, so the global key doesn’t apply to it |
Add the exception you need in that domain’s dictionary |
| -1200 even though the server is https | The server only supports TLS 1.0 or 1.1, or its cipher suites don’t meet ATS | Upgrade the server configuration. Use NSExceptionMinimumTLSVersion temporarily |
| With the proxy on, every HTTPS request fails with -1202 or -1200 | The proxy certificate doesn’t have full trust | Turn it on in Settings > General > About > Certificate Trust Settings |
| With the proxy on, requests fail with -1004 or -1001 | The proxy isn’t running, the IP or port is wrong, or the firewall blocks it | Open an http page in Safari to check the proxy |
| The certificate is trusted but your own API still fails with -999 | The app’s certificate pinning is active | Turn off pinning in the Debug configuration |
| -1003, the internal domain works at the office but not elsewhere | Internal domains don’t resolve outside your network | Use a public domain, or reconnect to the internal network to test |
| -1009 while other apps on the phone have network access | Network access is turned off for this app, for example under Settings > Cellular | Turn on network access for the app in Settings |
| After turning off the proxy, no app on the phone can reach the network | The Wi-Fi proxy setting still points at the computer | Set Configure Proxy back to Off |
For an error code that isn’t in the table, look up its value in Foundation’s NSURLError.h header to find the constant name, then use the steps above to see where iOS network debugging shows the request stopping.
FAQ
What does NSURLErrorDomain -1022 mean?
-1022 is NSURLErrorAppTransportSecurityRequiresSecureConnection, which means ATS blocked the request, usually because the URL uses http. Move the endpoint to https. If you can’t, add NSExceptionAllowsInsecureHTTPLoads for that one domain under NSExceptionDomains in Info.plist.
Should I use NSAllowsArbitraryLoads or NSExceptionDomains?
Prefer NSExceptionDomains, which opens only the domains you need while the app’s other connections stay protected by ATS. NSAllowsArbitraryLoads turns off ATS for the whole app. According to Apple’s documentation, both need a justification at App Review, and a justification for turning ATS off entirely is harder to make.
Every HTTPS request on my iPhone fails while using a proxy. What do I do?
First confirm the proxy’s root certificate is installed, then turn on full trust for it in Settings > General > About > Certificate Trust Settings, which is the step people miss most. If the certificate is trusted and only your own API still fails, check whether the app pins certificates and turn pinning off in the Debug configuration.
Why can’t my app reach the test server on my computer on iOS 17?
According to Apple’s documentation, from iOS 17 ATS no longer allows direct connections to IP addresses by default. Add the test server’s IP address to NSExceptionDomains, preferably only in the Info.plist used by the Debug configuration, so it doesn’t ship in release builds.
How can I see an iPhone app’s network requests without a proxy tool?
Log the request path, status code and error code with Logger in the app, and record phase timings with URLSessionTaskMetrics. Read them in KXBuild Console while debugging, or with kxdevice ostrace filtered by subsystem when not debugging. You can also record requests to a HAR file with kxdevice instruments har.