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
- curl -I the failing URL and read Content-Type; fix the server to send the correct media MIME type
- 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
- Fix or bypass the captive portal / intercepting proxy causing HTML responses
- 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
- Configure the contentTypePredicate to whitelist exactly the types your CDN serves
- curl -I your media URLs in CI to catch Content-Type misconfigurations early
- Treat text/html responses as captive-portal/error-page signals in player error handling
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
- ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED
- ERROR_CODE_IO_BAD_HTTP_STATUS
- ERROR_CODE_FAILED_RUNTIME_CHECK
- Passed buffer is not a direct ByteBuffer
- No license URL
AI-assisted analysis of google/ExoPlayer@dd430f7053 (2026-08-14).
Data as JSON: /api/errors/9adc6801e7de0e37.
Report an issue: GitHub.