Why does a React Native app's fetch to a plain http:// URL fail in a release build, and how should you allow it safely?
answer
- the OS refuses it, not fetch
- iOS: App Transport Security by default
- Android: cleartext blocked from API 28
- exception for one domain, never global
- debug builds allow local Metro traffic
basics
~10 sBoth platforms block cleartext HTTP by default: iOS through App Transport Security, Android from API level 28. The fix is HTTPS; failing that, a narrow per-domain exception, never NSAllowsArbitraryLoads or app-wide cleartext.
solid answer
~40 sReact Native's `fetch` runs on the platform networking stack, `NSURLSession` on iOS and OkHttp on Android, so the operating system's transport policy applies. **iOS App Transport Security** requires HTTPS by default, and **Android blocks cleartext traffic by default from API level 28**; the request fails with a generic network error and no HTTP status. It works in development because the React Native template sets `NSAllowsLocalNetworking` on iOS, and since 0.82 the React Native Gradle plugin sets the `usesCleartextTraffic` manifest placeholder to `true` for debug builds and `false` for release. The right fix is to serve the endpoint over HTTPS. If you truly cannot, add an exception for that one domain (`NSExceptionDomains` on iOS, a `domain-config` with `cleartextTrafficPermitted` on Android) rather than `NSAllowsArbitraryLoads` or `android:usesCleartextTraffic="true"` for the whole app.
code
xml · 8 lines<?xml version="1.0" encoding="utf-8"?>
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">status.partner.example</domain>
</domain-config>
</network-security-config>go deeper
Remember that both iOS and Android block plain http by default and that the fix is HTTPS, not a global switch.
Explain ATS keys and Android's cleartext switches, why debug builds behave differently, and how to write a single-domain exception.
Audit a release build's Info.plist and network security config for broad exceptions, and keep them from creeping in through generated native files.
Set the policy that no user or payment data may ever travel over a cleartext exception, and decide who approves any remaining legacy host.
## The symptom A payments app calls a partner's status endpoint at `http://status.partner.example`. In development it works; in the release build the call fails with a generic network error and no HTTP status, on both platforms. Nothing is wrong with the JavaScript: the **operating system** refused the connection. React Native does not implement HTTP itself. `fetch` is a polyfill over React Native's networking module, which uses **`NSURLSession`** on iOS and **OkHttp** on Android. Both sit under the platform's transport-security policy. ## iOS: App Transport Security **App Transport Security (ATS)** is on by default and requires HTTPS for connections made through `NSURLSession`. It is configured in `Info.plist` under the `NSAppTransportSecurity` dictionary: | Key | Effect | Use it for | |---|---|---| | `NSAllowsArbitraryLoads` | turns ATS off broadly | almost never; the React Native docs warn App Store review expects a justification | | `NSExceptionDomains` | per-domain exceptions, such as `NSExceptionAllowsInsecureHTTPLoads` for one host | a single legacy host you cannot move to HTTPS | | `NSAllowsLocalNetworking` | relaxes ATS for resources on the local network | development against Metro and local servers | The React Native iOS template ships with `NSAllowsArbitraryLoads` set to `false`, with a comment warning that changing it risks rejection, and `NSAllowsLocalNetworking` set to `true`, which is why Metro on your machine works. ## Android: cleartext traffic From **API level 28**, Android blocks cleartext (non-TLS) traffic by default. Two switches control it: - `android:usesCleartextTraffic` on the `<application>` element: an app-wide yes or no. - A **network security config** file (`res/xml/network_security_config.xml`, referenced by `android:networkSecurityConfig`), whose `cleartextTrafficPermitted` attribute can be set per domain in a `domain-config`. Since **React Native 0.82**, the template uses one manifest with `android:usesCleartextTraffic="${usesCleartextTraffic}"`, and the React Native Gradle plugin fills the placeholder: `true` for `debug` and `debugOptimized`, `false` for `release`. Before 0.82 a separate debug manifest set it to `true`. Either way, cleartext works in debug so the app can reach Metro, and is off in release. ## Fixing it properly 1. **Move the endpoint to HTTPS.** For a payments app this is the only acceptable answer for anything carrying user or payment data. 2. **If a third party cannot**, scope the exception to that host: an `NSExceptionDomains` entry on iOS, a `domain-config` with `cleartextTrafficPermitted="true"` for that domain on Android. 3. **Never** ship `NSAllowsArbitraryLoads: true` or `android:usesCleartextTraffic="true"` to make one call work. 4. In an Expo project, set these through app config and plugins instead of editing the generated native files, which prebuild would overwrite; `expo-build-properties`, for example, has an `android.usesCleartextTraffic` option. ## Checking a release build before it ships Debug builds hide this class of bug, so check the release configuration directly: 1. Build and run a **release** variant on a device and exercise every endpoint, including third-party ones called from JavaScript. 2. Inspect the built app's `Info.plist` for `NSAllowsArbitraryLoads` and for any `NSExceptionDomains` entry nobody can explain. 3. Inspect the merged Android manifest for the resolved `usesCleartextTraffic` value and for the network security config it references. 4. In Expo projects, run prebuild and review the generated native files, because that is what the build actually uses. ## Why the defaults are right Cleartext HTTP can be read and modified by anyone on the network path: a hotel Wi-Fi, a compromised router. A payments app that accepts one cleartext response can be fed a forged status or a redirect. The platform defaults exist so that every app gets this protection without opting in, and every exception you add is a place where it is switched off. ## Common mistakes - Testing only debug builds, where cleartext and local networking are allowed, and discovering the failure in production. - Adding an app-wide flag instead of a domain exception. - Blaming `fetch` or CORS: React Native's `fetch` does not enforce CORS; the block comes from the OS policy.
- Why does the same http call work against Metro in a debug build?The React Native iOS template sets `NSAllowsLocalNetworking` to `true`, which lets local addresses through ATS, and on Android the Gradle plugin sets `usesCleartextTraffic` to `true` for debug builds (since 0.82 through a manifest placeholder). Release builds get `false`, so the same call fails there.
- Is `NSAllowsArbitraryLoads` ever acceptable?Rarely, and it must be justified: the React Native docs point out App Store review expects a reason for disabling ATS, and the template warns against flipping it. For a known host, an `NSExceptionDomains` entry does the job without turning the protection off for every other connection.
saying these in an interview costs you the question
- The http failure is a CORS error from React Native's fetch.
- Setting NSAllowsArbitraryLoads is the standard fix for one host.
- Android allows cleartext by default in release builds.
- If it works in debug it will work in release.
- A network security config cannot scope cleartext to one domain.