square/okhttp · error · IOException

Unexpected code

Error message

Unexpected code 

What it means

Recipe-level guard inside the asynchronous Callback.onResponse: after the call succeeds at the transport layer, response.isSuccessful() is checked and a java.io.IOException("Unexpected code " + response) is thrown for non-2xx status. Because this runs on OkHttp's dispatcher thread, the exception is not propagated to your code — it is logged and swallowed by the Callback contract unless you handle it. Throwing inside onResponse defeats the purpose of onFailure, which is only invoked for transport failures (no connection, no response).

Source

Thrown at samples/guide/src/main/java/okhttp3/recipes/AsynchronousGet.java:42

import okhttp3.Response;
import okhttp3.ResponseBody;

public final class AsynchronousGet {
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        try (ResponseBody responseBody = response.body()) {
          if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

          Headers responseHeaders = response.headers();
          for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
          }

          System.out.println(responseBody.string());
        }
      }
    });
  }

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

View on GitHub (pinned to 4fc0831380)

Solutions

  1. Do not throw inside onResponse — log response.code() and surface the error via your own error callback or a CompletableFuture.completeExceptionally(...).
  2. Verify the URL still resolves to the expected plaintext document (curl it).
  3. If the body is unexpected, also check response.body().contentType() and contentLength().
  4. Use https:// instead of http:// to avoid redirect/parked-page surprises.

Example fix

// before
@Override public void onResponse(Call call, Response response) throws IOException {
  try (ResponseBody responseBody = response.body()) {
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
    ...
  }
}

// after
@Override public void onResponse(Call call, Response response) throws IOException {
  try (ResponseBody responseBody = response.body()) {
    if (!response.isSuccessful()) {
      resultFuture.completeExceptionally(
          new IOException("HTTP " + response.code() + " for " + response.request().url()));
      return;
    }
    ...
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// Surface async errors via a future instead of throwing on the dispatcher thread
CompletableFuture<String> future = new CompletableFuture<>();
client.newCall(request).enqueue(new Callback() {
  @Override public void onFailure(Call call, IOException e) { future.completeExceptionally(e); }
  @Override public void onResponse(Call call, Response response) throws IOException {
    try (ResponseBody body = response.body()) {
      if (!response.isSuccessful()) {
        future.completeExceptionally(new HttpException(response.code(), response.request().url().toString()));
        return;
      }
      future.complete(body.string());
    }
  }
});

Try / catch

// Callback-based: do NOT throw inside onResponse
@Override public void onResponse(Call call, Response response) throws IOException {
  try (ResponseBody body = response.body()) {
    if (!response.isSuccessful()) {
      errorHandler.accept(new HttpException(response.code(), response.request().url().toString()));
      return;
    }
    successHandler.accept(body.string());
  }
}

Prevention

When it happens

Trigger: client.newCall(request).enqueue(callback) against http://publicobject.com/helloworld.txt. Fires when the server returns 3xx not followed, 4xx, or 5xx — e.g. the site redirects http→https (handled by default), the path is gone (404), or the host is parked/changed (403/451). A network failure instead goes to onFailure, never reaching this line.

Common situations: publicobject.com being a moving target (the recipe domain has changed ownership/status over time); copying enqueue() code expecting throws to surface in the caller (they won't — the callback thread eats them); using http:// (not https) and hitting an ISP/hosting redirect page that returns 200 HTML, masking the issue.

Related errors


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