square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
Thrown in the Progress recipe: `if (!response.isSuccessful()) throw new IOException("Unexpected code " + response)`. A network interceptor wraps the response body in a ProgressResponseBody that reports bytes-read to a listener. The guard fires if the underlying server response is non-2xx. Note the progress body is still installed even on error responses, so the listener will fire for the error body bytes too.
Solutions
- Check response.code() before trusting the progress totals; an error body will produce misleading byte counts.
- Confirm the resource path still exists.
- Only forward to your ProgressListener when response.isSuccessful() if you only care about success bodies.
- Branch on the code instead of throwing so progress + status are both observable.
Example fix
// before
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
// after
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.err.println("HTTP " + response.code() + " (progress totals were for the error body)");
return;
}
System.out.println(response.body().string());
} Defensive patterns
Strategy: validation
Validate before calling
// Only trust progress totals on success; check status first.
if (!response.isSuccessful()) { /* error body; ignore progress totals */ return; } Type guard
static boolean progressReflectsTarget(Response r, long expectedLength) {
return r.isSuccessful() && (r.body().contentLength() == -1 || r.body().contentLength() == expectedLength);
} Try / catch
try (Response r = client.newCall(request).execute()) {
if (!r.isSuccessful()) { /* status; progress listener reported error-body bytes */ return; }
} catch (IOException e) {
// transport error
} Prevention
- Check response.code() before trusting progress byte totals; error bodies mislead.
- Confirm the resource path still exists.
- Only forward to your listener when successful if you only care about success bodies.
- Inspect contentLength() to validate the listener's reported length.
When it happens
Trigger: GET https://publicobject.com/helloworld.txt returns 404/403/5xx; the ProgressResponseBody wraps whatever body the server returned (possibly an error page), so the listener reports bytes for the error body and then this line throws.
Common situations: Sample host removed helloworld.txt; the progress numbers look wrong because they are measuring an HTML error page rather than the expected file; network interceptor wrapping changing timing.
Related errors
AI-assisted analysis of square/okhttp@91a8b34c6f (2026-08-10).
Data as JSON: /api/errors/163741737ff66ad8.
Report an issue: GitHub.
Appendix: 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 91a8b34c6f)