net::ERR_CLEARTEXT_NOT_PERMITTED — ошибка Android-приложения: оно попыталось открыть адрес по http без шифрования, а сетевая политика приложения такой трафик запрещает. Чаще всего её видно на экране WebView — встроенного браузера, через который приложения показывают веб-страницы. Сайт при этом может работать: в обычном браузере он откроется.
Мы разобрали исходный код Android, Chromium и OkHttp и прогнали настоящий системный код Android 16 и 17 на 11 вариантах файла network_security_config и 14 адресах.
Короткий ответ
Причина почти всегда одна из трёх:
- Приложение собрано с targetSdkVersion 28 или выше, а адрес
начинается с
http://. С Android 9 такой трафик по умолчанию запрещён. - В манифесте есть network_security_config, и для нужного хоста в нём
нет
cleartextTrafficPermitted="true". Атрибутandroid:usesCleartextTrafficв этом случае не действует. - Правило в файле написано так, что ни с чем не совпадает: звёздочка,
диапазон адресов, адрес со схемой
http://. Система такие записи принимает без ошибки.
Правильное исправление — перевести адрес на https. Разрешать HTTP стоит только для конкретных хостов, которые без него не работают.
Что означает ошибка
В Chromium ошибка описана в файле net/base/net_error_list.h:
// The request was blocked by system policy disallowing some or all cleartext
// requests. Used for NetworkSecurityPolicy on Android.
NET_ERROR(CLEARTEXT_NOT_PERMITTED, -29)
Запрос заблокирован системной политикой, которая запрещает часть или все
незашифрованные запросы. Политика — это NetworkSecurityPolicy Android. В
справке Android под cleartext понимается любой трафик без TLS: HTTP, FTP,
а в описании NetworkSecurityPolicy перечислены ещё XMPP, IMAP и SMTP без
шифрования.
Одна и та же проверка выглядит по-разному в зависимости от того, какая библиотека делает запрос:
| Где | Как выглядит |
|---|---|
| WebView | страница ошибки с кодом net::ERR_CLEARTEXT_NOT_PERMITTED |
| HttpURLConnection (встроенный в Android) | java.io.IOException: Cleartext HTTP traffic to example.com not permitted |
| OkHttp 3, 4, 5 | java.net.UnknownServiceException: CLEARTEXT communication to example.com not permitted by network security policy |
| DownloadManager, MediaPlayer | отказ системных компонентов, по справке атрибута usesCleartextTraffic |
Текст для HttpURLConnection взят из класса CleartextURLFilter в
HttpHandler.java встроенного в Android OkHttp, текст для OkHttp — из
RealRoutePlanner.kt. На стенде запрос через системный HttpHandler к
запрещённому хосту вернул ровно эту строку.
Где проверка стоит в WebView
В url_request_http_job.cc Chromium проверка выполняется до создания
сетевого запроса: если адрес не https и политика Android запрещает хост,
вместо запроса возвращается ошибка −29. Перед ней стоит проверка HSTS:
если сетевой стек знает, что хост работает только по https, адрес сначала
переписывается на https, и до запрета дело не доходит.
Сама проверка в Chromium по умолчанию выключена. Её включает WebView: в
aw_content_browser_client.cc написано, что для приложений с target API
уровня O (Android 8, API 26) и выше запрет соблюдается. Справка атрибута
usesCleartextTraffic говорит то же: WebView учитывает его для
приложений с targetSdkVersion 26 и выше.
Важное следствие: запрос не уходит в сеть, даже за DNS. На стенде без
сети разрешённый хост упал на разрешении имени (UnknownHostException),
а запрещённый — сразу на проверке политики. В журнале сервера такого
запроса не будет.
Почему HTTP запрещён по умолчанию
Правило по умолчанию задают две части системы.
Парсер манифеста (ParsingPackageUtils.java) выставляет флаг
usesCleartextTraffic, если атрибут не указан: true при
targetSdkVersion меньше 28, false — при 28 и выше. Затем, если файла
network_security_config нет, ManifestConfigSource берёт значение этого
флага как разрешение для всех хостов.
Если файл есть, разрешение по умолчанию берётся из
NetworkSecurityConfig.getDefaultBuilder: HTTP разрешён только при
targetSdkVersion ниже 28 (Android 9) и не для мгновенных приложений.
Флаг из манифеста при этом не читается совсем.
| targetSdkVersion | Файл network_security_config | usesCleartextTraffic | HTTP к хосту не из файла |
|---|---|---|---|
| 27 и ниже | нет | не указан | разрешён |
| 27 и ниже | нет | false | запрещён |
| 28 и выше | нет | не указан | запрещён |
| 28 и выше | нет | true | разрешён |
| 28 и выше | есть | true | запрещён |
| 27 и ниже | есть | любое | разрешён |
Пятая строка — частая ловушка: разработчик ставит
usesCleartextTraffic="true", добавляет файл ради сертификатов, и HTTP
снова перестаёт работать. В справке это сказано одной фразой: атрибут
игнорируется на Android 7.0 и выше, если указан файл network_security_config.
Стенд: код Android 16 и 17 на 11 конфигурациях
Эмулятор Android без аппаратной виртуализации не запустить, поэтому мы
взяли сборки системного кода android-all для Android 16 и 17 из Maven
Central — тот же код фреймворка, на котором работают тесты Robolectric.
Классы XmlConfigSource, ApplicationConfig и NetworkSecurityConfig
работали без изменений; подменили только системные часы, номер процесса и
разбор IP-адреса, которые на обычной Java недоступны.
Каждый файл разбирался кодом Android, затем для 14 адресов вызывался
isCleartextTrafficPermitted(host) — тот же метод, который вызывают
WebView, HttpURLConnection и OkHttp. Приложение: targetSdkVersion 28,
usesCleartextTraffic="true", сборка не отладочная.
| Файл | api.example.com | cdn.api.example.com | secure.example.com | 10.0.2.2 | 192.168.1.10 | localhost (16 / 17) |
|---|---|---|---|---|---|---|
| только domain-config для api.example.com | да | нет | нет | нет | нет | нет / да |
| base-config с разрешением | да | да | да | да | да | да / да |
| example.com с поддоменами | да | да | да | нет | нет | нет / да |
| example.com с поддоменами, вложенный secure.example.com с запретом | да | да | нет | нет | нет | нет / да |
| domain-config без атрибута до base-config с разрешением | да | да | да | да | да | да / да |
| debug-overrides с разрешением | нет | нет | нет | нет | нет | нет / да |
| 10.0.2.2 и 192.168.1.0/24 | нет | нет | нет | да | нет | нет / да |
*.example.com и http://example.org | нет | нет | нет | нет | нет | нет / да |
| base-config с запретом, api.example.com с разрешением | да | нет | нет | нет | нет | нет / да |
| localhost с запретом | нет | нет | нет | нет | нет | нет / нет |
| example.com дважды | ошибка разбора |
«Да» — HTTP разрешён, «нет» — будет ERR_CLEARTEXT_NOT_PERMITTED. Без
файла вовсе, с флагом false, Android 16 запретил все 14 адресов, а
Android 17 — все, кроме localhost, 127.0.0.1 и ::1.
Для сравнения тот же первый файл при targetSdkVersion 27 разрешил все адреса: разрешение по умолчанию для старых приложений никуда не делось.
Пять ловушек в network_security_config
Звёздочки, диапазоны и схемы молча не работают. Записи
*.example.com, 192.168.1.0/24 и http://example.org система приняла
без единой ошибки и ни с одним адресом не сопоставила. Код
ApplicationConfig.getConfigForHostname сравнивает имя целиком или, при
includeSubdomains="true", проверяет, что адрес оканчивается на
.имя. Для всех поддоменов пишут сам домен с includeSubdomains="true".
Для локальной сети — каждый адрес отдельно.
debug-overrides не разрешает HTTP. В коде XmlConfigSource из раздела
debug-overrides переносятся только доверенные сертификаты. Атрибут
cleartextTrafficPermitted="true" в нём не действовал ни в обычной, ни в
отладочной сборке. Чтобы HTTP работал только при разработке, кладут
отдельный файл в ресурсы отладочной сборки (src/debug/res/xml/).
Повтор домена ломает весь файл. Если один домен указан в двух
domain-config, разбор падает с ошибкой «example.com has already been
specified». В XmlConfigSource такая ошибка оборачивается в исключение
«Failed to parse XML configuration from …» — сетевая политика приложения
не загружается вовсе.
Побеждает самое длинное совпадение. При вложенном domain-config
secure.example.com с запретом внутри example.com с разрешением
запрещены secure.example.com и его поддомены, а www.example.com
разрешён. Порядок в файле не важен: domain-config, записанный до
base-config, всё равно унаследовал от него разрешение.
Проверка без имени хоста строже. Метод
isCleartextTrafficPermitted() без аргумента вернул «нет» для всех
файлов, где хотя бы одному домену HTTP запрещён. Им пользуются старые
библиотеки: OkHttp на Android 6 вызывает именно его, потому что проверка
по хосту появилась в Android 7.
Android 17: localhost и конец usesCleartextTraffic
В коде Android 17 появились две перемены, которых нет в Android 16.
Неявное правило для localhost. Если приложение не задало правило для
localhost, система добавляет его сама: HTTP разрешён. Справка Android
перечисляет, что считается локальным адресом: localhost, ip6-localhost
и числовые адреса петли, например 127.0.0.1 и [::1]. На стенде так и
вышло. Свой запрет для localhost это правило не перекрывает: файл с
cleartextTrafficPermitted="false" для localhost в Android 17 запретил
все три адреса.
Адрес 10.0.2.2, по которому эмулятор обращается к компьютеру
разработчика, под правило не попадает: он не локальный для самого
телефона. Для сервера разработки на компьютере разрешение по-прежнему
пишут явно.
Отказ от usesCleartextTraffic. В ManifestConfigSource Android 17
появилась проверка изменения совместимости
DEPRECATE_USES_CLEARTEXT_TRAFFIC (номер 415007211): когда она
включена, флаг из манифеста считается равным false. В сборке, которую
мы проверяли, изменение выключено. Справка атрибута предупреждает, что он
будет игнорироваться для приложений с targetSdkVersion 38 и выше, и
советует задавать HTTP через network_security_config.
Как исправить ошибку разработчику
Лучший вариант — https
Если сервер умеет https, поменяйте адрес в приложении. Ошибка исчезнет без единой строки конфигурации, а данные перестанут идти открытым текстом. Если сервер не умеет https, сертификат Let's Encrypt выпускается бесплатно. Сертификат должен быть настоящим: самоподписанный приведёт к другой ошибке — NET::ERR_CERT_AUTHORITY_INVALID.
Разрешить HTTP одному хосту
Файл res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">legacy.example.com</domain>
</domain-config>
</network-security-config>
И ссылка на него в манифесте:
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
После этого usesCleartextTraffic в манифесте можно убрать: при наличии
файла он всё равно не действует.
Сервер разработки и эмулятор
Для эмулятора, который обращается к компьютеру по 10.0.2.2, и для
телефона в локальной сети указывают адреса по одному:
<domain-config cleartextTrafficPermitted="true">
<domain>10.0.2.2</domain>
<domain>192.168.1.10</domain>
</domain-config>
Такой файл лучше держать только в отладочной сборке, а в выпускной оставить запрет.
Разрешить всё
<base-config cleartextTrafficPermitted="true" />
Справка Android называет это небезопасной настройкой, которой стоит избегать. Вариант оправдан, только если приложение открывает адреса, которые заранее неизвестны, например браузер.
Найти, откуда идёт HTTP
- В отладочной сборке включите
StrictModeсdetectCleartextNetwork()— справка Android рекомендует его для поиска незашифрованного трафика. - Проверьте перенаправления. Сервер, который отвечает на https
перенаправлением на
http://, приведёт WebView к той же ошибке: каждый новый адрес проходит проверку заново. - Проверьте адреса из ответов сервера: ссылки на картинки, видео и файлы
часто хранятся в базе со схемой
http://.
Что делать пользователю
Со стороны телефона ошибку не исправить: запрет записан в самом приложении, и никакой настройки Android для него нет. Поможет обновление приложения, если разработчик уже исправил адрес. Если обновления нет, напишите в поддержку приложения и укажите адрес, который не открылся. Очистка кэша, переустановка и другая сеть ничего не изменят: запрос не уходит в сеть вовсе.
Что это значит для владельца сайта
Если ваш сайт открывают в приложении через WebView — в мобильном банке,
агрегаторе, мессенджере, — адрес с http:// там не откроется. Решение на
стороне сайта одно: работать по https и везде ставить ссылки с https.
Если сайт до сих пор отвечает по http, мы разбирали последствия в статье
«Сайт до сих пор на http».
На поисковых роботов ошибка не влияет: это проверка внутри Android, а не ответ сервера.
Чем она отличается от похожих ошибок
| Ошибка | Что не так |
|---|---|
| ERR_CLEARTEXT_NOT_PERMITTED | приложение запрещает HTTP без шифрования |
| ERR_UNKNOWN_URL_SCHEME | WebView не умеет открывать ссылку с такой схемой |
| NET::ERR_CERT_AUTHORITY_INVALID | https есть, но сертификату не доверяют |
| ERR_NETWORK_ACCESS_DENIED | система не дала открыть сетевое соединение |
| ERR_BLOCKED_BY_CLIENT | запрос заблокировало расширение или само приложение |
Границы замера
Стенд собран 20.10.2026: системный код из сборок android-all 16-robolectric-13921718 и 17-robolectric-15733970 (Maven Central) на Java 21, файлы конфигурации разбирались настоящим XmlConfigSource, проверка — настоящим ApplicationConfig, текст исключения — настоящим HttpHandler. На телефоне и в эмуляторе не проверяли: аппаратной виртуализации на сервере нет. Поведение WebView и OkHttp описано по их исходному коду, а не по запуску.
Источники
- Chromium:
net/base/net_error_list.h,net/url_request/url_request_http_job.cc,android_webview/browser/aw_content_browser_client.cc - Android:
NetworkSecurityConfig.java,ManifestConfigSource.java,XmlConfigSource.java,ApplicationConfig.java,ParsingPackageUtils.java; встроенный OkHttp —HttpHandler.java - OkHttp:
RealRoutePlanner.kt,AndroidPlatform.kt - Справка Android: «Network security configuration», атрибут
android:usesCleartextTrafficэлемента application, изменения поведения Android 9
Похожие ошибки
- ERR_UNKNOWN_URL_SCHEME — WebView получил ссылку, которую не умеет открыть.
- NET::ERR_CERT_AUTHORITY_INVALID — сертификат выдан центром, которому не доверяют.
- ERR_SSL_KEY_USAGE_INCOMPATIBLE — назначение ключа в сертификате не подходит к шифру.
- ERR_BLOCKED_BY_CLIENT — запрос заблокирован на стороне клиента.
Коротко
ERR_CLEARTEXT_NOT_PERMITTED значит, что Android-приложение запретило себе HTTP без шифрования. С targetSdkVersion 28 это поведение по умолчанию, а если в манифесте есть network_security_config, атрибут usesCleartextTraffic не действует. Лучшее исправление — https; если нельзя, разрешение пишут в domain-config точным именем или адресом. Звёздочки, диапазоны и debug-overrides для этого не работают. В Android 17 localhost разрешён по умолчанию, а 10.0.2.2 — нет.
