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
- Do not throw inside onResponse — log response.code() and surface the error via your own error callback or a CompletableFuture.completeExceptionally(...).
- Verify the URL still resolves to the expected plaintext document (curl it).
- If the body is unexpected, also check response.body().contentType() and contentLength().
- 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
- Never throw inside Callback.onResponse expecting the caller to see it — use onFailure semantics or a CompletableFuture.
- Use https:// to avoid redirect/parked-page responses.
- Verify the URL still serves the expected content before relying on it.
- Distinguish transport failures (onFailure) from HTTP errors (onResponse non-2xx).
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.