square/okhttp · error · IOException

Unexpected code

Error message

Unexpected code 

What it means

Recipe-level guard inside a client that installs a network interceptor wrapping the ResponseBody in a ProgressResponseBody (ForwardingSource that counts bytes read). After the wrapped response is returned, response.isSuccessful() is checked; non-2xx throws java.io.IOException("Unexpected code " + response). The progress wrapping does not change the status, so this is a normal HTTP-error guard layered over a progress-tracking body.

Source

Thrown at samples/guide/src/main/java/okhttp3/recipes/Progress.java:72

          if (contentLength != -1) {
            System.out.format("%d%% done\n", (100 * bytesRead) / contentLength);
          }
        }
      }
    };

    OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(chain -> {
          Response originalResponse = chain.proceed(chain.request());
          return originalResponse.newBuilder()
              .body(new ProgressResponseBody(originalResponse.body(), progressListener))
              .build();
        })
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

  public static void main(String... args) throws Exception {
    new Progress().run();
  }

  private static class ProgressResponseBody extends ResponseBody {

    private final ResponseBody responseBody;
    private final ProgressListener progressListener;
    private BufferedSource bufferedSource;

    ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
      this.responseBody = responseBody;
      this.progressListener = progressListener;

View on GitHub (pinned to 4fc0831380)

Solutions

  1. Inspect response.code() and the progress listener's reported contentLength.
  2. Do not assume bytesRead corresponds to the successful payload — guard for error responses.
  3. Verify the URL still serves the expected plaintext.

Example fix

// before
try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  ...
}

// after
try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) {
    // the progress listener has already counted the error body bytes
    throw new IOException("HTTP " + response.code() + " for " + request.url());
  }
  ...
}
Defensive patterns

Strategy: validation

Validate before calling

// The ProgressResponseBody wraps error bodies too; account for that in the listener
@Override public void update(long bytesRead, long contentLength, boolean done) {
  // only treat as a successful download if the caller later confirms response.isSuccessful()
  if (contentLength == -1) { /* unknown size; likely an error or chunked body */ }
  ...
}

Try / catch

try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) {
    // the progress listener has already counted the error body's bytes
    throw new HttpException(response.code(), response.message());
  }
  // only now treat progress numbers as authoritative for the successful payload
}

Prevention

When it happens

Trigger: GET https://publicobject.com/helloworld.txt with the progress interceptor. Fires on any non-2xx (404, 503, etc.). Note the ProgressResponseBody wraps whatever body the server returns, so for an error response the progress listener will report the error body's bytes before this line throws.

Common situations: Origin returning 5xx during maintenance; expecting the progress listener to only fire for successful bodies (it fires for error bodies too); host redirect/parked page.

Related errors


AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04). Data as JSON: /data/errors/163741737ff66ad8.json. Report an issue: GitHub.