google/ExoPlayer · error · HttpDataSource.CleartextNotPermittedException

ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED

ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED

Error message

Cleartext HTTP traffic not permitted. See https://developer.android.com/guide/topics/media/issues/cleartext-not-permitted

What it means

CleartextNotPermittedException (PlaybackException error code ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED) thrown by CronetDataSource when the connection failure message contains 'err_cleartext_not_permitted'. Since Android 9 (API 28), plain http:// requests are blocked by default unless the app opts in, so any http data source open fails with this error.

Solutions

  1. Serve the media over https:// — this is the correct production fix and the one the error URL recommends
  2. For debug builds only, set android:usesCleartextTraffic="true" in the manifest, or a network security config permitting cleartext for the specific dev domain
  3. Audit playlist/manifest segment URIs and redirects to ensure none downgrade to http
  4. Pre-check the scheme: refuse or rewrite http:// DataSpecs before calling dataSource.open()

Example fix

<!-- before: no opt-in, http blocked on API 28+ -->
<application ...>

<!-- after (debug only): targeted network security config -->
<!-- res/xml/network_security_config.xml -->
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">dev.example.com</domain>
  </domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application android:networkSecurityConfig="@xml/network_security_config" ...>
Defensive patterns

Strategy: fallback

Validate before calling

Uri uri = Uri.parse(dataSpec.uri);
if ("http".equals(uri.getScheme())
    && Build.VERSION.SDK_INT >= 28
    && !NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(uri.getHost())) {
  // rewrite to https or fail with a clear message BEFORE dataSource.open()
  uri = uri.buildUpon().scheme("https").build();
}

Try / catch

try {
  dataSource.open(dataSpec);
} catch (CleartextNotPermittedException e) {
  // the URL is plain http and Android blocks it: switch origin to https and retry
  DataSpec httpsSpec = dataSpec.buildUpon()
      .setUri(dataSpec.uri.replaceFirst("http://", "https://"))
      .build();
  dataSource.open(httpsSpec);
}

Prevention

When it happens

Trigger: Opening a DataSpec with an http:// (non-TLS) URL through CronetDataSource on API 28+ without a cleartext opt-in; a redirect from https:// to an http:// host; playlists whose segment URIs are absolute http:// while the manifest was https://.

Common situations: Test/dev servers on plain http; content from CDNs that serve http segments; app targetSdk >= 28 where android:usesCleartextTraffic defaults to false.

Related errors


AI-assisted analysis of google/ExoPlayer@dd430f7053 (2026-08-14). Data as JSON: /api/errors/04942092a7a14c72. Report an issue: GitHub.

Appendix: source

Thrown at extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java:557

      currentUrlRequest = urlRequest;
    } catch (IOException e) {
      if (e instanceof HttpDataSourceException) {
        throw (HttpDataSourceException) e;
      } else {
        throw new OpenException(
            e, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, Status.IDLE);
      }
    }
    urlRequest.start();

    transferInitializing(dataSpec);
    try {
      boolean connectionOpened = blockUntilConnectTimeout();
      @Nullable IOException connectionOpenException = exception;
      if (connectionOpenException != null) {
        @Nullable String message = connectionOpenException.getMessage();
        if (message != null && Ascii.toLowerCase(message).contains("err_cleartext_not_permitted")) {
          throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
        }
        throw new OpenException(
            connectionOpenException,
            dataSpec,
            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
            getStatus(urlRequest));
      } else if (!connectionOpened) {
        // The timeout was reached before the connection was opened.
        throw new OpenException(
            new SocketTimeoutException(),
            dataSpec,
            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
            getStatus(urlRequest));
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      // An interruption means the operation is being cancelled, in which case this exception should
      // not cause the player to fail. If it does, it likely means that the owner of the operation

View on GitHub (pinned to dd430f7053)