{"record":{"id":"2be2205211ab8adf","repo":"google/ExoPlayer","slug":"error-code-io-bad-http-status","errorCode":"ERROR_CODE_IO_BAD_HTTP_STATUS","errorMessage":"Response code: ${responseCode}","messagePattern":"Response code: (.+?)","errorType":"http","errorClass":"HttpDataSource.InvalidResponseCodeException","httpStatus":null,"severity":"error","filePath":"extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java","lineNumber":611,"sourceCode":"          opened = true;\n          transferStarted(dataSpec);\n          return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;\n        }\n      }\n\n      byte[] responseBody;\n      try {\n        responseBody = readResponseBody();\n      } catch (IOException e) {\n        responseBody = Util.EMPTY_BYTE_ARRAY;\n      }\n\n      @Nullable\n      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","sourceCodeStart":593,"sourceCodeEnd":629,"githubUrl":"https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java#L593-L629","documentation":"InvalidResponseCodeException (ERROR_CODE_IO_BAD_HTTP_STATUS) thrown by CronetDataSource when the server answered with a non-2xx/3xx response code. For 416 the exception carries a DataSourceException(ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) cause, mapping the classic 'range not satisfiable' case to a distinct playback error code.","triggerScenarios":"Server returns 403 (expired signed URL / hotlink protection), 404 (moved media), 416 (range beyond EOF, typically after stale cached positions or an incorrect DataSpec.length), or 5xx; expired auth tokens in request headers.","commonSituations":"Signed CDN URLs past their expiry; live stream segment removed after sliding window; resuming a download at a position the current file no longer supports; DRM/token middleware rejecting requests.","solutions":["Inspect responseCode: 403/401 means refresh the signed URL or auth header and retry with a new DataSpec; 404 means the media moved or expired; 416 means the requested range is beyond the current resource length","For 416, reset any persisted position/length state for that resource and reopen from position 0 (or the new content length)","Verify the request URL, headers and Range logic against a curl reproduction of the failing request","Handle InvalidResponseCodeException in a custom HttpDataSource.ResponseValidator / player error callback to refresh credentials and recover instead of failing playback"],"exampleFix":"// before: no recovery on HTTP error\nplayer.prepare(new MediaItem.Builder().setUri(url).build());\n\n// after: react to 416 by reopening from zero\nplayer.addListener(new Player.Listener() {\n  @Override\n  public void onPlayerError(PlaybackException error) {\n    if (error.errorCode == PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) {\n      player.seekTo(0); // stale range; restart the resource\n      player.prepare();\n    }\n  }\n});","handlingStrategy":"try-catch","validationCode":"// HEAD-check before streaming (optional, costs one round trip)\nHttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();\nc.setRequestMethod(\"HEAD\");\nint code = c.getResponseCode();\nif (code == 403) refreshSignedUrl();\nelse if (code == 404) removeDeadItemFromQueue();\nelse if (code == 416) resetPersistedPosition(url);","typeGuard":null,"tryCatchPattern":"try {\n  dataSource.open(dataSpec);\n} catch (InvalidResponseCodeException e) {\n  switch (e.responseCode) {\n    case 401: case 403: refreshCredentialsAndRetry(); break; // re-sign URL, rebuild DataSpec\n    case 404: skipToNextItem(); break;\n    case 416: dataSource.open(dataSpec.buildUpon().setPosition(0).setLength(C.LENGTH_UNSET).build()); break;\n    default: throw e;\n  }\n}","preventionTips":["Refresh signed URLs/tokens before they expire rather than after a 403","On 416, reset stored positions/lengths for the resource instead of resuming the stale range","Reproduce failing requests with curl -H 'Range: ...' to confirm server-side range behavior"],"tags":["android","network","cronet","http-status"],"backgroundTag":null,"analyzedSha":"dd430f7053a1a3958deea3ead6a0565150c06bfc","analyzedAt":"2026-08-14T12:22:02.982Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}