google/ExoPlayer · error · HttpDataSource.InvalidContentTypeException

ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE

ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE

Error message

Invalid content type: ${contentType}

What it means

InvalidContentTypeException (ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE) thrown by CronetDataSource when the response's Content-Type header fails the configured contentTypePredicate. The predicate defaults to accepting only known media types, so serving HTML (error pages), XML, or unexpected types over a media URL trips this check before any bytes are read.

Source

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

      IOException cause =
          responseCode == 416
              ? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
              : null;
      throw new InvalidResponseCodeException(
          responseCode,
          responseInfo.getHttpStatusText(),
          cause,
          responseHeaders,
          dataSpec,
          responseBody);
    }

    // Check for a valid content type.
    Predicate<String> contentTypePredicate = this.contentTypePredicate;
    if (contentTypePredicate != null) {
      @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
      if (contentType != null && !contentTypePredicate.apply(contentType)) {
        throw new InvalidContentTypeException(contentType, dataSpec);
      }
    }

    // If we requested a range starting from a non-zero position and received a 200 rather than a
    // 206, then the server does not support partial requests. We'll need to manually skip to the
    // requested position.
    long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;

    // Calculate the content length.
    if (!isCompressed(responseInfo)) {
      if (dataSpec.length != C.LENGTH_UNSET) {
        bytesRemaining = dataSpec.length;
      } else {
        long contentLength =
            HttpUtil.getContentLength(
                getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
                getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
        bytesRemaining =

View on GitHub (pinned to dd430f7053)

Solutions

  1. curl -I the failing URL and read Content-Type; fix the server to send the correct media MIME type
  2. Configure the predicate to accept your real types when building the data source: new CronetDataSource.Builder(...).setContentTypePredicate(type -> true) or a whitelist including your types
  3. Fix or bypass the captive portal / intercepting proxy causing HTML responses
  4. For adaptive streams, make sure the playlist and segments come from the same correctly-configured origin

Example fix

// before: default predicate rejects non-standard content types
CronetDataSource dataSource = new CronetDataSource.Builder(cronetEngine, executor)
    .build();

// after: accept the content types you actually serve
CronetDataSource dataSource = new CronetDataSource.Builder(cronetEngine, executor)
    .setContentTypePredicate(contentType ->
        contentType.startsWith("audio/")
            || contentType.startsWith("video/")
            || contentType.equals(MimeTypes.APPLICATION_M3U8)
            || contentType.startsWith("application/octet-stream"))
    .build();
Defensive patterns

Strategy: validation

Validate before calling

// Before playback, confirm the server labels the media acceptably
HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
String contentType = c.getHeaderField("Content-Type");
// Configure the same predicate on the data source that accepts this value
Predicate<String> ok = type -> type.startsWith("audio/")
    || type.startsWith("video/")
    || type.equals("application/octet-stream");
if (!ok.apply(contentType)) fixServerOrPredicate();

Try / catch

try {
  dataSource.open(dataSpec);
} catch (InvalidContentTypeException e) {
  String type = e.contentType; // what the server actually sent
  if (type.startsWith("text/html")) {
    // captive portal / error page: surface a network message, do not retry blindly
    showError(R.string.network_intercepted);
  }
}

Prevention

When it happens

Trigger: Media URL actually returns text/html (captive portal, proxy error page, wrong link); server mislabels an mp4 as application/octet-stream when the predicate does not allow it; using the default predicate with non-standard content types like audio/aacp or application/x-mpegURL variants.

Common situations: Hotel/VPN captive portals intercepting the request; CDN misconfiguration; developer hosts raw files on a static server that guesses MIME types from extensions; custom protocols behind an HTTP facade.

Related errors


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