{"record":{"id":"9adc6801e7de0e37","repo":"google/ExoPlayer","slug":"error-code-io-invalid-http-content-type","errorCode":"ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE","errorMessage":"Invalid content type: ${contentType}","messagePattern":"Invalid content type: (.+?)","errorType":"http","errorClass":"HttpDataSource.InvalidContentTypeException","httpStatus":null,"severity":"error","filePath":"extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java","lineNumber":625,"sourceCode":"      IOException cause =\n          responseCode == 416\n              ? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)\n              : null;\n      throw new InvalidResponseCodeException(\n          responseCode,\n          responseInfo.getHttpStatusText(),\n          cause,\n          responseHeaders,\n          dataSpec,\n          responseBody);\n    }\n\n    // Check for a valid content type.\n    Predicate<String> contentTypePredicate = this.contentTypePredicate;\n    if (contentTypePredicate != null) {\n      @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);\n      if (contentType != null && !contentTypePredicate.apply(contentType)) {\n        throw new InvalidContentTypeException(contentType, dataSpec);\n      }\n    }\n\n    // If we requested a range starting from a non-zero position and received a 200 rather than a\n    // 206, then the server does not support partial requests. We'll need to manually skip to the\n    // requested position.\n    long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;\n\n    // Calculate the content length.\n    if (!isCompressed(responseInfo)) {\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        long contentLength =\n            HttpUtil.getContentLength(\n                getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),\n                getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));\n        bytesRemaining =","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java#L607-L643","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: default predicate rejects non-standard content types\nCronetDataSource dataSource = new CronetDataSource.Builder(cronetEngine, executor)\n    .build();\n\n// after: accept the content types you actually serve\nCronetDataSource dataSource = new CronetDataSource.Builder(cronetEngine, executor)\n    .setContentTypePredicate(contentType ->\n        contentType.startsWith(\"audio/\")\n            || contentType.startsWith(\"video/\")\n            || contentType.equals(MimeTypes.APPLICATION_M3U8)\n            || contentType.startsWith(\"application/octet-stream\"))\n    .build();","handlingStrategy":"validation","validationCode":"// Before playback, confirm the server labels the media acceptably\nHttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();\nString contentType = c.getHeaderField(\"Content-Type\");\n// Configure the same predicate on the data source that accepts this value\nPredicate<String> ok = type -> type.startsWith(\"audio/\")\n    || type.startsWith(\"video/\")\n    || type.equals(\"application/octet-stream\");\nif (!ok.apply(contentType)) fixServerOrPredicate();","typeGuard":null,"tryCatchPattern":"try {\n  dataSource.open(dataSpec);\n} catch (InvalidContentTypeException e) {\n  String type = e.contentType; // what the server actually sent\n  if (type.startsWith(\"text/html\")) {\n    // captive portal / error page: surface a network message, do not retry blindly\n    showError(R.string.network_intercepted);\n  }\n}","preventionTips":["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"],"tags":["android","network","cronet","content-type","http"],"backgroundTag":null,"analyzedSha":"dd430f7053a1a3958deea3ead6a0565150c06bfc","analyzedAt":"2026-08-14T12:22:02.982Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}