square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
This is a sample-recipe guard (not built into OkHttp): after client.newCall(request).execute() returns a Response, the code checks response.isSuccessful() (true only for HTTP status 200-299) and throws java.io.IOException("Unexpected code " + response) for anything else. The Response.toString() appended to the message contains the full status line, URL, and headers, which is the only diagnostic you get. OkHttp itself never throws this — it is the recipe author's convention for turning a transport-success-but-HTTP-error into an exception.
Source
Thrown at samples/guide/src/main/java/okhttp3/recipes/AccessHeaders.java:35
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public final class AccessHeaders {
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/lysine-dev/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}
}
public static void main(String... args) throws Exception {
new AccessHeaders().run();
}
}
View on GitHub (pinned to 4fc0831380)
Solutions
- Inspect response.code() and response.headers() (X-RateLimit-Remaining, X-RateLimit-Reset) before throwing to see the real cause.
- If 403 rate-limited, authenticate the request with a GitHub token (Authorization: Bearer <token>) to raise the limit, or add caching/backoff.
- If 404, verify the repo owner/name in the URL is correct (the recipe pins lysine-dev/okhttp).
- Keep the User-Agent header (recipe sets 'OkHttp Headers.java'); GitHub requires a non-empty UA.
- Branch on response.isSuccessful() instead of throwing, so 3xx/4xx/5xx can be handled individually.
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.code() == 403) {
throw new IOException("GitHub rate-limited; remaining="
+ response.header("X-RateLimit-Remaining"));
}
if (!response.isSuccessful()) {
throw new IOException("GitHub " + response.code() + " for " + response.request().url());
}
...
} Defensive patterns
Strategy: validation
Validate before calling
// Validate the GitHub target before relying on the response
Request request = new Request.Builder()
.url("https://api.github.com/repos/lysine-dev/okhttp/issues")
.header("User-Agent", "MyApp/1.0 (contact@example.com)") // GitHub requires this
.header("Authorization", "Bearer " + token) // lifts 60/hr cap
.header("Accept", "application/vnd.github+json")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.code() == 403 || response.code() == 429) {
// respect X-RateLimit-Reset before retrying
}
if (!response.isSuccessful()) { /* handle by code */ }
} Try / catch
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
switch (response.code()) {
case 403: throw new RateLimitedException(response.header("X-RateLimit-Reset"));
case 404: throw new NotFoundException(response.request().url().toString());
default: throw new HttpException(response.code(), response.message());
}
}
// use response
} Prevention
- Always send a User-Agent header to GitHub.
- Authenticate requests to lift the 60/hr unauthenticated cap.
- Branch on response.code() instead of a single isSuccessful() throw.
- Check X-RateLimit-Remaining and back off before the limit resets.
When it happens
Trigger: GET https://api.github.com/repos/lysine-dev/okhttp/issues with custom Accept + User-Agent headers. Surfaces when GitHub returns 404 (repo renamed/deleted/non-existent), 301/302 the recipe does not follow (it will follow by default), 403 (unauthenticated rate limit exceeded — 60 req/hr per IP), or 410. response.code() will be the actual failing status.
Common situations: Hitting GitHub's unauthenticated rate limit (403 with X-RateLimit-Remaining: 0); copying the recipe but forgetting the User-Agent header (GitHub rejects requests without one); targeting a repo slug that does not exist or was renamed; running behind a shared NAT IP where the 60/hr limit is consumed by others.
Related errors
AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04).
Data as JSON: /data/errors/cb977d990cba03b3.json.
Report an issue: GitHub.