square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
The canonical OkHttp 'Synchronous Get' recipe: after `client.newCall(request).execute()`, the code calls `response.isSuccessful()` and throws `IOException("Unexpected code " + response)` for any non-2xx status before printing headers and body. This is the most minimal form of the idiom and the one most often copy-pasted.
Solutions
- Open the URL in a browser or with `curl -I` to confirm it still returns 200; if not, point the recipe at a URL you control.
- Replace the blanket throw with a switch on `response.code()` so 4xx is reported distinctly from 5xx and you can retry only 5xx/429.
- If the host is behind a proxy, configure `.proxy(...)` or `ProxySelector` on the `OkHttpClient` builder.
- For production code, distinguish transport errors (`IOException` from `execute()`) from HTTP-status errors (this throw) so they can be handled with different policies.
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()) {
int code = response.code();
if (code >= 200 && code < 300) {
// success path: read headers + body
} else if (code >= 500 || code == 429) {
throw new IOException("Transient server error " + code + " for " + request.url());
} else {
throw new IOException("Client error " + code + " " + response.message()
+ " for " + request.url());
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: cheap reachability check (optional) so you fail before the call.
URI uri = URI.create("https://publicobject.com/helloworld.txt");
try {
InetAddress.getAllByName(uri.getHost()); // throws if DNS dead
} catch (UnknownHostException e) {
throw new IllegalStateException("Cannot resolve " + uri.getHost(), e);
}
// Post-call: classify instead of blanket-throw.
try (Response response = client.newCall(request).execute()) {
int c = response.code();
if (c >= 200 && c < 300) {
// read headers + body
} else if (c == 404) {
throw new ContentMissingException(uri);
} else if (c >= 500 || c == 429) {
throw new TransientHttpException(c, response.header("Retry-After"));
} else {
throw new IOException("HTTP " + c + " " + response.message());
}
} Type guard
private static boolean isBodyReadable(Response r) {
return r.isSuccessful() && r.body() != null;
}
private static boolean isTransient(int code) {
return code == 429 || (code >= 500 && code < 600);
} Try / catch
int attempts = 0;
while (true) {
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
// read body
break;
}
if (!isTransient(response.code())) {
throw new IOException("Non-retryable HTTP " + response.code());
}
// transient: honor Retry-After if present, exponential backoff otherwise
long backoff = Math.min(30_000L, 500L * (1L << attempts));
Thread.sleep(backoff);
} catch (IOException e) {
// transport failure — also retryable on transient conditions
if (++attempts > MAX_ATTEMPTS) throw e;
}
} Prevention
- Replace the blanket `!isSuccessful()` throw with explicit handling per status class (2xx/3xx/4xx/5xx).
- Retry only transient failures (5xx, 429) with bounded exponential backoff; do not retry 4xx.
- Treat DNS/TLS errors (thrown by `execute()`) separately from HTTP-status outcomes.
- For long-lived URLs you don't control, add a fallback URL or graceful degradation.
When it happens
Trigger: A GET to `https://publicobject.com/helloworld.txt` returns non-2xx: 404 if the path/file is moved or removed, 451/403 under geo or WAF blocks, 500/502/503 during origin outages, or 407 if a proxy demands authentication. DNS/TLS/connection failures throw a different exception (e.g. `UnknownHostException`, `SSLHandshakeException`) before this line is reached; the `Unexpected code` throw is purely an HTTP-status outcome.
Common situations: Hard-coded external sample URL going offline or being reorganized; running in a restricted network where the host is blocked; copy-pasting the recipe as production error handling and treating any 3xx-not-followed or 4xx as a crash; not realizing OkHttp follows redirects/30x by default so this line rarely fires for redirects.
Related errors
AI-assisted analysis of square/okhttp@91a8b34c6f (2026-08-10).
Data as JSON: /api/errors/500954d40d7dc143.
Report an issue: GitHub.
Appendix: source
Thrown at samples/guide/src/main/java/okhttp3/recipes/SynchronousGet.java:33
*/
package okhttp3.recipes;
import java.io.IOException;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public final class SynchronousGet {
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
}
public static void main(String... args) throws Exception {
new SynchronousGet().run();
}
}
View on GitHub (pinned to 91a8b34c6f)