Fix "Cleartext HTTP traffic to [domain] not permitted" on Android

intermediate๐Ÿ“ฑ Android2026-04-07| Android 9.0 (API 28) and above, affects all HTTP (non-HTTPS) network requests from Android apps

Error Message

java.io.IOException: Cleartext HTTP traffic to [domain] not permitted
#android-security#network-security-config#http-error#android-9.0

What happened

Switched a staging API from HTTPS to plain HTTP temporarily, app started crashing on Android. Logcat showed:

java.io.IOException: Cleartext HTTP traffic to api.staging.example.com not permitted

Android 9 (API 28) made this a hard block โ€” no warnings, just a crash. Before Pie, HTTP worked fine. Starting from API 28, any http:// request to a domain not explicitly allowed gets killed by the OS. There's no runtime workaround.

Three scenarios where this hits hardest: a backend that's temporarily HTTP-only during migration, a local dev server at 192.168.x.x, or a third-party SDK still phoning home over HTTP. The last one is the sneaky one โ€” you didn't write that code.

Debug process

Pin down the exact domain triggering the block first. Pull full logcat and filter for CLEARTEXT:

adb logcat | grep -i cleartext

You'll see something like:

W/NetworkSecurityConfig: Cleartext HTTP traffic to api.staging.example.com not permitted

That's your target domain. Sometimes it's not your code at all โ€” embedded analytics SDKs like Firebase Crashlytics older versions or ad SDKs are common culprits.

Next, check whether a network_security_config.xml already exists:

find app/src/main/res -name "network_security_config.xml"

If it's already there, someone set up network security rules and your domain just isn't in the list.

Solution

Option 1: Allow specific domains (recommended)

Create app/src/main/res/xml/network_security_config.xml if it doesn't exist:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">api.staging.example.com</domain>
        <domain includeSubdomains="true">192.168.1.100</domain>
    </domain-config>
</network-security-config>

Register it in AndroidManifest.xml inside the <application> tag:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

Surgical and auditable. Production HTTPS traffic stays fully protected โ€” only the domains you list get HTTP access.

Option 2: Allow HTTP for debug builds only

HTTP only needed in development? Scope the permissive config to debug builds. Create app/src/debug/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

Add the android:networkSecurityConfig attribute only in the debug manifest (app/src/debug/AndroidManifest.xml). Release builds never see it.

Option 3: Nuclear option (not for production)

Drop this directly into AndroidManifest.xml:

<application
    android:usesCleartextTraffic="true"
    ...>

Allows HTTP everywhere. Never ship this โ€” Google Play will flag it, and you're removing transport security for every user. Use it only to confirm HTTP is actually the problem, then remove it.

Option 4: Switch to HTTPS (the real fix)

For servers you control, force HTTPS. For local dev, grab a self-signed cert or run ngrok to get a proper HTTPS tunnel in under a minute. For third-party SDKs still on HTTP, file a bug with the vendor โ€” that's on them.

Verification

Rebuild and install:

./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Trigger the network call in the app. The Cleartext HTTP traffic warning should be gone from logcat. If you're using OkHttp, add a logging interceptor to see the full request complete:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BASIC))
    .build();

A 200 response confirms the fix. An IOException means the domain still isn't whitelisted.

Tips

Can't figure out which library is making the HTTP call? Enable StrictMode in your Application class during debug:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
    .detectNetwork()
    .penaltyLog()
    .build());

This logs the full stack trace โ€” right down to the class that initiated the request. Much faster than grepping through 15 SDKs manually.

Working with IP ranges for local dev servers or corporate proxies? The Subnet Calculator on ToolCraft makes it quick to check CIDR ranges. Handy when you're unsure whether 192.168.x.x and 10.x.x.x fall under the same subnet before writing your domain-config rules.

Lessons learned

  • Keep android:usesCleartextTraffic="true" out of the main manifest entirely. Debug-only scope if you absolutely need it.
  • Per-domain config in network_security_config.xml is the right tool โ€” explicit, reviewable in code, and invisible to production traffic.
  • Run adb logcat | grep -i cleartext before every release. Third-party SDKs are a silent source of this error that often gets missed until someone reviews the Play Store security report.
  • Android's Network Security Config docs are unusually good. A full read covers certificate pinning, trust anchors, and debug overrides โ€” all the options you'll eventually need.

Related Error Notes