square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
Same OkHttp idiom: after a synchronous `execute()`, `response.isSuccessful()` is checked and `IOException("Unexpected code " + response)` is thrown on any non-2xx status. Here it sits inside a 5-iteration loop hitting `https://api.github.com/search/repositories?q=http`, alternating between the plain client and a client that adds a `REWRITE_CACHE_CONTROL_INTERCEPTOR` network interceptor forcing `Cache-Control: max-age=60`.
Solutions
- Add authentication: set `.header("Authorization", "token <your-PAT>")` (or `Authorization: Basic <base64(user:token)>`) — raises search limit to 30/min and the core limit to 5000/hr.
- Throttle the loop (e.g. space requests >= 6s apart) or honor `X-RateLimit-Reset` before retrying.
- Before throwing, log `response.code()` and `response.headers("X-RateLimit-Remaining")` to distinguish rate-limit (403) from genuine search errors (422).
- If copying the interceptor, restrict it to non-production or remove it; forcing `max-age=60` masks server freshness signals.
Example fix
// before
Request request = new Request.Builder()
.url("https://api.github.com/search/repositories?q=http")
.build();
...
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
// after
Request request = new Request.Builder()
.url("https://api.github.com/search/repositories?q=http")
.header("Authorization", "token " + System.getenv("GITHUB_TOKEN"))
.header("Accept", "application/vnd.github+json")
.build();
...
if (!response.isSuccessful()) {
throw new IOException("GitHub " + response.code() + " " + response.message()
+ " (rate limit remaining: " + response.header("X-RateLimit-Remaining") + ")");
} Defensive patterns
Strategy: validation
Validate before calling
// Validate auth + rate-limit budget before the call.
String token = System.getenv("GITHUB_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException("GITHUB_TOKEN required for Search API without 403.");
}
// Back off based on last-known remaining budget (read from a previous response).
if (lastRemaining != null && lastRemaining <= 1) {
long sleepMs = Math.max(0, lastResetEpochMillis - System.currentTimeMillis());
Thread.sleep(sleepMs);
}
Request request = new Request.Builder()
.url("https://api.github.com/search/repositories?q=http")
.header("Authorization", "token " + token)
.header("Accept", "application/vnd.github+json")
.build();
// After: capture rate-limit headers for the next iteration.
try (Response response = clientForCall.newCall(request).execute()) {
lastRemaining = Integer.valueOf(response.header("X-RateLimit-Remaining", "-1"));
lastResetEpochMillis = Long.parseLong(response.header("X-RateLimit-Reset", "0")) * 1000L;
if (response.code() == 403 && "0".equals(response.header("X-RateLimit-Remaining"))) {
throw new RateLimitedException("GitHub rate limit exhausted; resets at " + lastResetEpochMillis);
}
if (!response.isSuccessful()) throw new IOException("HTTP " + response.code());
} Type guard
private static boolean isRateLimited(Response r) {
return r.code() == 403
&& "0".equals(r.header("X-RateLimit-Remaining"));
}
private static boolean isCacheUsable(Response r) {
return r.isSuccessful() && r.cacheResponse() != null;
} Try / catch
try (Response response = clientForCall.newCall(request).execute()) {
if (response.code() == 403 && isRateLimited(response)) {
// honor X-RateLimit-Reset, then retry the current iteration
throw new RateLimitedException(response.header("X-RateLimit-Reset"));
}
if (!response.isSuccessful()) {
throw new IOException("GitHub " + response.code() + " " + response.message());
}
System.out.println("Network: " + (response.networkResponse() != null));
} catch (RateLimitedException e) {
sleepUntilReset(e.resetEpochMillis());
i--; // redo this iteration
} Prevention
- Always authenticate GitHub API calls, even in samples.
- Honor `X-RateLimit-Remaining` and `Retry-After` headers; do not tight-loop the Search endpoint.
- Keep the cache directory writable and unique per process to avoid evictAll() races.
- Don't copy `REWRITE_CACHE_CONTROL_INTERCEPTOR` into production — it deliberately lies about freshness.
When it happens
Trigger: GitHub's Search API rate-limits unauthenticated clients to 10 requests/minute (and secondary rate limits apply to the Search endpoint specifically); running the loop 5+ times against an already-warm IP quickly yields HTTP 403 with `X-RateLimit-Remaining: 0`. Also fires on 422 (invalid query), 451, or 5xx during GitHub incidents. Cache-directory permission problems surface as separate IOExceptions but can leave the response chain in a non-2xx state.
Common situations: Running the recipe repeatedly during development without a GitHub token; CI runners sharing a NAT IP exhausting the unauthenticated allowance; cache dir on read-only filesystem or non-empty after a previous crashed run (`cache.evictAll()` at line 39 then fails); `REWRITE_CACHE_CONTROL_INTERCEPTOR` being copied into production code where it lies to the cache about freshness.
Related errors
AI-assisted analysis of square/okhttp@91a8b34c6f (2026-08-10).
Data as JSON: /api/errors/b51eafd63aed9d3e.
Report an issue: GitHub.
Appendix: source
Thrown at samples/guide/src/main/java/okhttp3/recipes/RewriteResponseCacheControl.java:68
Request request = new Request.Builder()
.url("https://api.github.com/search/repositories?q=http")
.build();
OkHttpClient clientForCall;
if (i == 2) {
// Force this request's response to be written to the cache. This way, subsequent responses
// can be read from the cache.
System.out.println("Force cache: true");
clientForCall = client.newBuilder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
} else {
System.out.println("Force cache: false");
clientForCall = client;
}
try (Response response = clientForCall.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(" Network: " + (response.networkResponse() != null));
System.out.println();
}
}
}
public static void main(String... args) throws Exception {
new RewriteResponseCacheControl(new File("RewriteResponseCacheControl.tmp")).run();
}
}
View on GitHub (pinned to 91a8b34c6f)