square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
RewriteResponseCacheControl.java:68 uses the same isSuccessful() guard against a call to https://api.github.com/search/repositories?q=http. The IOException 'Unexpected code ' surfaces the Response toString, hiding the real GitHub status code. Notably, the recipe's REWRITE_CACHE_CONTROL_INTERCEPTOR (lines 28-33) is documented as 'dangerous' and overwrites the server's Cache-Control with 'max-age=60', so a stale or error response can be served from cache as a 200 on later iterations, while the first un-cached hit can surface the original non-2xx.
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 4fc0831380)
Solutions
- Add an Authorization header (e.g. a GitHub token via .header("Authorization", "token <gpa_token>")) to lift search to 30 req/min and avoid 403.
- Log response.code(), response.headers().get("X-RateLimit-Remaining"), and response.body().string() before throwing to confirm rate-limit vs. real upstream error.
- Remove or scope REWRITE_CACHE_CONTROL_INTERCEPTOR — it is explicitly labelled dangerous; do not ship cache-control rewriting in production code.
- Verify the cache directory (new File("RewriteResponseCacheControl.tmp")) is writable and unique per test run to avoid cross-run cache poisoning.
Example fix
// before
try (Response response = clientForCall.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(" Network: " + (response.networkResponse() != null));
}
// after
try (Response response = clientForCall.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("GitHub " + response.code()
+ " rateLimit=" + response.header("X-RateLimit-Remaining"));
}
System.out.println(" Network: " + (response.networkResponse() != null));
} Defensive patterns
Strategy: validation
Validate before calling
// Check GitHub rate-limit headers cheaply before doing the real search
Request probe = new Request.Builder()
.url("https://api.github.com/rate_limit")
.header("Authorization", "token " + token)
.build();
try (Response r = client.newCall(probe).execute()) {
String remaining = r.header("X-RateLimit-Remaining");
if (remaining == null || Integer.parseInt(remaining) <= 0) {
throw new IllegalStateException("GitHub search quota exhausted");
}
} Try / catch
try (Response response = clientForCall.newCall(request).execute()) {
if (response.code() == 403 && response.header("X-RateLimit-Remaining") != null) {
long reset = Long.parseLong(response.header("X-RateLimit-Reset"));
throw new IOException("GitHub rate-limited until " + new java.util.Date(reset * 1000L));
}
if (!response.isSuccessful()) {
throw new IOException("GitHub " + response.code());
}
process(response);
} Prevention
- Always send an Authorization header for GitHub search to raise the 10/min unauthenticated ceiling.
- Do not ship REWRITE_CACHE_CONTROL_INTERCEPTOR — cache-control rewriting masks upstream errors and stale data.
- Use a unique cache directory per test run to avoid cross-run cache pollution.
- Check X-RateLimit-Remaining on every GitHub call and back off when near zero.
When it happens
Trigger: Hitting GitHub's /search/repositories without an Authorization header — GitHub returns 403 with rate-limit headers for unauthenticated search (10 requests/minute). Iteration i==2 (line 55) adds the network interceptor that rewrites Cache-Control; subsequent iterations read from cache, masking transient upstream 5xx errors as cached 200s or surfacing them only on the seeding call. A corporate proxy returning an HTML block page (status 407/502) also trips this.
Common situations: Running the recipe in a loop exceeding the unauthenticated search rate limit; sharing a network/IP with CI runners that exhaust the GitHub quota; the 'dangerous' cache-control rewrite producing stale data that downstream code misreads; misconfigured OkHttpClient.cache directory (lines 37-39) causing evictions/permission errors.
Related errors
AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04).
Data as JSON: /data/errors/b51eafd63aed9d3e.json.
Report an issue: GitHub.