Fixing iOS App Transport Security (ATS) Blocking HTTP API Calls in React Native
The exact same fetch call succeeds on Android and fails on iOS with a generic "Network request failed" — no status code, no server-side log entry, nothing to debug in the API itself. On iOS, that error usually isn't the network or the server at all; it's App Transport Security blocking the request before it ever leaves the device.
The Problem
A React Native app makes a network request — often to a local development backend, an internal staging API, or a legacy service — and it works without issue on Android, both in the emulator and on a physical device. The identical request on iOS fails with a generic error like Network request failed or Could not connect to the server, with no HTTP status code, no timing that suggests a slow connection, and critically, nothing showing up in the backend's own access logs — because the request never actually reached the server at all.
Why It Happens
iOS blocks non-HTTPS connections by default, at the operating system level, before the app's own code runs
Since iOS 9, Apple's App Transport Security (ATS) has enforced that apps only make network connections using HTTPS with a reasonably modern TLS configuration, by default and without any way for an app to silently opt out. A plain http:// request — extremely common when pointing a React Native app at a local machine's IP address during development, or at an internal backend that was never migrated off HTTP — gets blocked at the OS networking layer, before it reaches whatever error handling exists in the app's own fetch or axios call.
The failure looks identical to a real network problem from inside the app
Because ATS intervenes below the level of the app's own networking code, the JavaScript layer receives a generic connection failure with no distinguishing detail — nothing that says "blocked by ATS" as opposed to "the server is actually unreachable." This is what makes the bug easy to misdiagnose as a backend or connectivity issue rather than a client-side security policy, especially since the same URL works fine when opened directly in a browser or hit from a non-iOS client.
The Fix
1. The correct fix for anything shipping to real users is HTTPS on the API itself
ATS exists as a legitimate security control, not an obstacle to route around — any backend an app talks to in production should be served over HTTPS with a valid, properly configured certificate. Treating an ATS block as a signal to disable the protection, rather than as a signal that an endpoint needs a real certificate, just defers a real security gap instead of closing it.
2. For local development only, scope an exception to the exact dev host — never disable ATS globally
<!-- ios/YourApp/Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>192.168.1.50</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
A scoped NSExceptionDomains entry allows plain HTTP for one named host or IP while leaving ATS's protection fully intact for every other connection the app makes — this is the right shape of exception for a local dev backend whose IP is known and doesn't change often.
3. Never ship NSAllowsArbitraryLoads: true to a release build or the App Store
<!-- Do NOT do this for a production app -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
This blanket flag disables ATS for every network connection the app makes, not just the one endpoint that actually needed an exception — it's a real, app-wide reduction in security, Apple's App Store review explicitly flags it, and it should never appear in a build meant for real users, even as a "temporary" fix during development that later gets forgotten.
4. Prefer an HTTPS-providing local tunnel over an ATS exception where practical
Tools that expose a local backend through a public HTTPS URL (rather than a raw local HTTP address) sidestep the ATS question entirely for development, since the app is then genuinely talking HTTPS the whole way — useful when the same build needs to work for multiple developers without each one adding their own machine's IP as an ATS exception.
Why This Works
Each of these approaches respects what ATS is actually checking for — a genuinely secure connection — rather than treating the block as noise to silence. Fixing the API to serve HTTPS closes the gap at its real source; a domain-scoped exception limits the reduced protection to exactly the one known, trusted development endpoint instead of the entire app; and avoiding NSAllowsArbitraryLoads in production keeps every other connection the app makes under ATS's actual protection, which is what stops a plaintext-downgrade or man-in-the-middle attack against a real user in the first place.
Conclusion
A generic "network request failed" on iOS that has no equivalent on Android, and no corresponding entry in the backend's access logs, is a strong signal that App Transport Security is blocking the connection before it leaves the device — not a real connectivity or server problem. The durable fix is HTTPS on the API; for local development, scope an ATS exception to the exact dev host rather than disabling the protection app-wide, and never let a blanket NSAllowsArbitraryLoads exception reach a production build.
