skip to content

Transport Rules & Pinning

iOS App Transport Security and Android's network security config block cleartext HTTP, and pinning a certificate or public key narrows trust further. Interviewers probe pin rotation and lock-out.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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?

level: middleimportance: must knowfreq 62%

answer

  1. the OS refuses it, not fetch
  2. iOS: App Transport Security by default
  3. Android: cleartext blocked from API 28
  4. exception for one domain, never global
  5. debug builds allow local Metro traffic

basics

~10 s

Both 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 s

React 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
<?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

for a junior

Remember that both iOS and Android block plain http by default and that the fix is HTTPS, not a global switch.

for a middle

Explain ATS keys and Android's cleartext switches, why debug builds behave differently, and how to write a single-domain exception.

for a senior

Audit a release build's Info.plist and network security config for broad exceptions, and keep them from creeping in through generated native files.

for a principal

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.
open as a page

A React Native payments app pins its API key and the server certificate is being replaced; how do you rotate without locking out users who never update?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Plan rotation around the oldest app still in use: renew with the same key where possible, and when the key must change, switch to a backup key whose pin already shipped. An expiration date on Android pins and a forced-update path limit lock-out.

open as a page

In a React Native payments app, how do you implement public-key pinning for the API on iOS and Android, and why ship a backup pin?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Pin the SHA-256 hash of the API's public key in native config: a pin-set in Android's network security config and NSPinnedDomains in iOS's Info.plist, or a pinning module. Always include a backup key's pin so rotation needs no emergency release.

open as a page

Should a React Native payments app pin certificates at all, and how would you decide?

level: principalimportance: should knowfreq 24%

basics

~20 s

Pin only when the threat justifies the operational cost: pinning stops a mis-issued or rogue-CA certificate for your own API, but risks locking users out on rotation. For a payments API you control, pinning keys with backups is often justified.

open as a page

In a React Native Android app, how do android:usesCleartextTraffic and network_security_config.xml relate, and what else can the config file express?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

usesCleartextTraffic is one app-wide flag; network_security_config.xml is a per-domain policy file referenced from the manifest. When the file is present it takes precedence, and it can also set trust anchors, debug-only overrides and certificate pins.

open as a page