"It worked perfectly in the simulator, but the moment I shipped it to TestFlight, the API calls just stopped — no error, no crash, nothing." I have lived this exact scenario. Around the time my indie app portfolio crossed 50 million cumulative downloads, I swapped out the measurement SDK for an ad network, and on launch day the app stayed silent: it never crashed, the spinner never stopped, the response simply never arrived. After three days of customer support tickets piling up, I traced the cause back to App Transport Security (ATS) quietly blocking HTTP traffic at the OS level.
This silent failure is especially painful in the Rork + EAS pipeline. Coming from web development, it feels reasonable to hit http://api.example.com directly. But on a release build the OS swallows the request before your JavaScript ever sees it, and even your try / catch blocks never fire. Below is the order in which I triage these incidents, plus an Info.plist that lets you grant the smallest possible exception while still passing Apple's review.
Why does it fail "silently"? ATS defaults explained
Since iOS 9, ATS enforces all of the following on every iOS app by default:
- Connections must use HTTPS
- TLS must be 1.2 or higher
- The full certificate chain must validate
- The cipher suite must support Forward Secrecy
Any traffic that violates these rules gets killed by the OS. fetch and axios are both backed by URLSession, so once the OS drops the request, your JavaScript layer often does not even receive an error object. You can wrap the call in try / catch all day long; if the connection never leaves the device, catch never fires.
There is a saying I inherited from my grandfathers, who were both temple carpenters in Japan: "If your first plane stroke is crooked, every later step inherits the warp." When the foundation of the network stack is wrong, the bug is invisible from any layer above.
Triage order — work from the bottom up
Without a clean error message, you cannot guess your way through Info.plist changes. You first need to nail down what the network is actually doing. Here is the order I follow on real incidents.
1. URL scheme and port
# Sweep every API string in your project
grep -rE 'https?://[^"]+' src/ app/ services/ 2>/dev/null | grep -v '\.test\.'If even one URL starts with http://, that is almost certainly your problem. The same applies to localhost if you are testing the Expo Dev Client against a real device — ATS exceptions are required.
2. Measure the TLS version and cipher suite
The URL might be HTTPS while the server's TLS configuration is too old to satisfy ATS. Confirm from a Mac terminal:
# Check that the server actually negotiates TLS 1.2
openssl s_client -connect api.example.com:443 -tls1_2 < /dev/null
# List the cipher suites the server offers (requires nmap)
nmap --script ssl-enum-ciphers -p 443 api.example.comIf you see Cipher is (NONE) or handshake failure, the server fails ATS's cipher requirements. No amount of Info.plist editing on the Rork side will save you — fix the server, or move to a different domain.
3. Confirm the exception actually shipped
Inspect the final Info.plist inside the build that EAS produced.
# Pull Info.plist from a TestFlight IPA
unzip -p "App.ipa" "Payload/*.app/Info.plist" | plutil -convert xml1 -o - -Look for the NSAppTransportSecurity key and verify its content is what you expect. With Rork projects, app.config.ts changes occasionally fail to propagate because of EAS caching, so this final check matters.
Writing a minimum-privilege ATS exception
This is the substance of the fix. Setting NSAllowsArbitraryLoads = true is not just a review risk — it actively exposes your users to MITM attacks. As an indie developer who wants the apps my children eventually inherit to be honest pieces of work, this is a line I will not cross casually. Apple's review team rejects builds that flip the global flag without a documented justification.
Pattern A: allow HTTP for one specific domain
Use this when a single legacy API or payment partner cannot serve HTTPS yet.
<!-- Info.plist (injected via app.config.ts ios.infoPlist) -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<!-- Allow HTTP traffic -->
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<!-- Lower the TLS floor for this domain only (optional) -->
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<!-- Set true only when you also need subdomains -->
<key>NSIncludesSubdomains</key>
<false/>
</dict>
</dict>
</dict>In Rork, you typically inject this from app.config.ts:
// app.config.ts
export default {
expo: {
ios: {
infoPlist: {
NSAppTransportSecurity: {
NSExceptionDomains: {
"legacy-api.example.com": {
NSExceptionAllowsInsecureHTTPLoads: true,
NSExceptionMinimumTLSVersion: "TLSv1.0",
NSIncludesSubdomains: false,
},
},
},
},
},
},
};Expected output: after expo prebuild, the generated ios/<App>/Info.plist should contain the same keys, verbatim.
Pattern B: relax media traffic only
If you are progressively downloading m3u8 / HLS streams, NSAllowsArbitraryLoadsInMedia exempts video traffic without touching your API surface. From experience with AdMob video ads, this exception has dramatically smaller side effects than the global one.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInMedia</key>
<true/>
</dict>Pattern C: relax WebView only
When the app itself talks HTTPS but you embed a WebView that loads external HTTP resources:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>This lets <img src="http://..."> and similar subresources load inside the WebView without opening the rest of the app to insecure connections.
Avoiding rejection at App Review
Apple's review team will ask why a build sets NSAllowsArbitraryLoads = true. Since the privacy manifest became mandatory, in my experience reviewers also look at ATS settings more often (I covered the privacy manifest side at Fixing privacy manifest API declaration errors during Rork app review).
Justifications that tend to work:
- Communication with a third-party legacy server you do not control (bonus points if you can show a migration timeline)
- Public infrastructure such as some regional broadcasting APIs that simply do not offer HTTPS
- A test server inside a closed network whose CA cannot be trusted by the device (must be disabled in production builds)
"We need it for video playback" is rarely accepted on its own — reviewers will usually push you toward the narrower NSAllowsArbitraryLoadsInMedia instead.
A small probe you can paste in
Here is a tiny utility I keep around to verify that the ATS exception actually took effect on a real device. It pokes both HTTPS and HTTP endpoints so you can see which side URLSession is killing.
// utils/atsProbe.ts
// Hit HTTPS and HTTP back to back to surface which side is being blocked
export async function atsProbe() {
const targets = [
{ label: "HTTPS", url: "https://httpbin.org/get" },
{ label: "HTTP", url: "http://httpbin.org/get" },
];
for (const t of targets) {
const t0 = Date.now();
try {
const res = await fetch(t.url, { method: "GET" });
console.log(`[ATS Probe] ${t.label} ok=${res.ok} status=${res.status} ms=${Date.now() - t0}`);
} catch (e: any) {
// When ATS blocks the call, the error message is usually "Network request failed"
console.log(`[ATS Probe] ${t.label} threw: ${e?.message ?? e} ms=${Date.now() - t0}`);
}
}
}Expected behaviour:
- No exception in place: HTTPS succeeds, HTTP throws
Network request failed - Pattern A with
httpbin.orgwhitelisted: both succeed - Pattern B (media only): the HTTP
fetchstill fails, which is the correct outcome
If "HTTPS works but HTTP fails," ATS is doing its job and you can scope your fix narrowly. If even the HTTPS call dies, the cause is somewhere else — proxy, DNS, or certificate pinning — and you should switch to a more general triage like Debugging network and API requests in Rork apps.
Operational rules I follow on shipped apps
Closing notes from running ATS exceptions on a portfolio that crossed 50 million cumulative downloads:
- New apps start with no
NSAllowsArbitraryLoads. With Cloudflare, Supabase, or any modern backend, HTTPS is enough on day one. - When an exception is introduced for legacy compatibility, both the release notes and the internal README must record the exempted domain and a removal target date.
- Every six months I run an inventory pass and forcibly retire exceptions that are still in place but no longer needed.
- After a server migration or CDN switch, re-run the TLS measurement the next day. The configuration drift is real.
ATS issues are loud when they hit production but cheap to prevent — a few lines of config and fifteen minutes of testing. If you also want to map the layer above this one, Triaging database backend connection errors in Rork iOS apps gives you a complementary view of the network stack.
Thank you for reading this through.