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

  1. 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.
  2. Throttle the loop (e.g. space requests >= 6s apart) or honor `X-RateLimit-Reset` before retrying.
  3. Before throwing, log `response.code()` and `response.headers("X-RateLimit-Remaining")` to distinguish rate-limit (403) from genuine search errors (422).
  4. 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

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)