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
- Inspect response.code() and the progress listener's reported contentLength.
- Do not assume bytesRead corresponds to the successful payload — guard for error responses.
- 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
- Do not assume progress bytes correspond to a successful payload — error bodies are wrapped too.
- Log response.code() alongside the progress listener to correlate partial reads with errors.
- Verify the URL still serves the expected content type and length.
- Handle contentLength == -1 (unknown / chunked) gracefully.
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.