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

  1. 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.
  2. Log response.code(), response.headers().get("X-RateLimit-Remaining"), and response.body().string() before throwing to confirm rate-limit vs. real upstream error.
  3. Remove or scope REWRITE_CACHE_CONTROL_INTERCEPTOR — it is explicitly labelled dangerous; do not ship cache-control rewriting in production code.
  4. 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

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.