{"record":{"id":"ed1fb5899a451a2f","repo":"google/ExoPlayer","slug":"error-code-failed-runtime-check","errorCode":"ERROR_CODE_FAILED_RUNTIME_CHECK","errorMessage":"HTTP request with non-empty body must set Content-Type","messagePattern":"HTTP request with non-empty body must set Content-Type","errorType":"validation","errorClass":"CronetDataSource.OpenException","httpStatus":null,"severity":"error","filePath":"extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java","lineNumber":827,"sourceCode":"            .setPriority(requestPriority)\n            .allowDirectExecutor();\n\n    // Set the headers.\n    Map<String, String> requestHeaders = new HashMap<>();\n    if (defaultRequestProperties != null) {\n      requestHeaders.putAll(defaultRequestProperties.getSnapshot());\n    }\n    requestHeaders.putAll(requestProperties.getSnapshot());\n    requestHeaders.putAll(dataSpec.httpRequestHeaders);\n\n    for (Entry<String, String> headerEntry : requestHeaders.entrySet()) {\n      String key = headerEntry.getKey();\n      String value = headerEntry.getValue();\n      requestBuilder.addHeader(key, value);\n    }\n\n    if (dataSpec.httpBody != null && !requestHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {\n      throw new OpenException(\n          \"HTTP request with non-empty body must set Content-Type\",\n          dataSpec,\n          PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK,\n          Status.IDLE);\n    }\n\n    @Nullable String rangeHeader = buildRangeRequestHeader(dataSpec.position, dataSpec.length);\n    if (rangeHeader != null) {\n      requestBuilder.addHeader(HttpHeaders.RANGE, rangeHeader);\n    }\n    if (userAgent != null) {\n      requestBuilder.addHeader(HttpHeaders.USER_AGENT, userAgent);\n    }\n    // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed\n    // (adjusting the code as necessary).\n    // Force identity encoding unless gzip is allowed.\n    // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {\n    //   requestBuilder.addHeader(\"Accept-Encoding\", \"identity\");","sourceCodeStart":809,"sourceCodeEnd":845,"githubUrl":"https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java#L809-L845","documentation":"OpenException (ERROR_CODE_FAILED_RUNTIME_CHECK) thrown by CronetDataSource while building the request when dataSpec.httpBody is non-null but no Content-Type header is present (after merging default and per-request properties). Cronet requires a content type for bodies, so the library fails fast before starting the request rather than surfacing a confusing native error later.","triggerScenarios":"DataSpec.Builder().setHttpRequestHeaders(...) with a body via setHttpBody but no \"Content-Type\" entry; POST-style requests (e.g. DRM license requests or custom auth) routed through CronetDataSource with only default headers that omit Content-Type; assuming the datasource adds a default content type.","commonSituations":"License/token request POSTs with JSON bodies; migrating from DefaultHttpDataSource (which tolerated the omission) to CronetDataSource; per-request headers overriding/removing the default Content-Type.","solutions":["Set Content-Type explicitly on the DataSpec: setHttpRequestHeaders(Map.of(\"Content-Type\", \"application/octet-stream\"))","Alternatively configure setDefaultRequestProperties(Pair.create(\"Content-Type\", \"application/octet-stream\")) on the CronetDataSource factory/builder for all requests with bodies","Check for the header before open(): if dataSpec.httpBody != null, ensure a Content-Type is merged in"],"exampleFix":"// before\nDataSpec dataSpec = new DataSpec.Builder()\n    .setUri(licenseUrl)\n    .setHttpBody(jsonBody.getBytes(UTF_8))\n    .build();\ndataSource.open(dataSpec); // throws: no Content-Type\n\n// after\nDataSpec dataSpec = new DataSpec.Builder()\n    .setUri(licenseUrl)\n    .setHttpRequestHeaders(\n        ImmutableMap.of(\"Content-Type\", \"application/json\"))\n    .setHttpBody(jsonBody.getBytes(UTF_8))\n    .build();\ndataSource.open(dataSpec);","handlingStrategy":"validation","validationCode":"DataSpec spec = dataSpec;\nif (spec.httpBody != null\n    && !spec.httpRequestHeaders.containsKey(\"Content-Type\")\n    && !defaultRequestProperties.containsKey(\"Content-Type\")) {\n  spec = spec.buildUpon()\n      .setHttpRequestHeaders(ImmutableMap.<String, String>builder()\n          .putAll(spec.httpRequestHeaders)\n          .put(\"Content-Type\", \"application/octet-stream\")\n          .buildOrThrow())\n      .build();\n}\ndataSource.open(spec);","typeGuard":null,"tryCatchPattern":"try {\n  dataSource.open(dataSpec);\n} catch (CronetDataSource.OpenException e) {\n  if (e.errorCode == PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK\n      && dataSpec.httpBody != null) {\n    // add Content-Type and retry\n    dataSource.open(dataSpec.buildUpon()\n        .setHttpRequestHeaders(ImmutableMap.of(\"Content-Type\", \"application/octet-stream\"))\n        .build());\n  } else {\n    throw e;\n  }\n}","preventionTips":["Any DataSpec with setHttpBody must also set a Content-Type header — pair them always","Set a default Content-Type via setDefaultRequestProperties for body-carrying pipelines (DRM, auth)","Unit-test request building paths that add bodies so the header is never forgotten"],"tags":["android","cronet","http","post-request","validation"],"backgroundTag":null,"analyzedSha":"dd430f7053a1a3958deea3ead6a0565150c06bfc","analyzedAt":"2026-08-14T12:22:02.982Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}