◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-10Intermediate

Why Your Rork iOS App's API Calls Fail Silently — Fixing App Transport Security the Right Way

When a Rork-built iOS app's API requests fail with no error and no log, App Transport Security (ATS) is almost always the culprit. Here is the minimum-privilege fix and an Info.plist that actually passes review.

Rork585iOS116App Transport SecurityATSNetwork3Troubleshooting43

"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.com

If 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.org whitelisted: both succeed
  • Pattern B (media only): the HTTP fetch still 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-05-18
Fixing Linking.canOpenURL False Returns on iOS in Rork
Production iOS builds silently route every user to the mobile web instead of opening TikTok or X? This is almost always a missing LSApplicationQueriesSchemes entry. Here is the full fix path for Rork.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.