{"record":{"id":"b51eafd63aed9d3e","repo":"square/okhttp","slug":"unexpected-code-b51eaf","errorCode":null,"errorMessage":"Unexpected code ","messagePattern":"Unexpected code ","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"samples/guide/src/main/java/okhttp3/recipes/RewriteResponseCacheControl.java","lineNumber":68,"sourceCode":"      Request request = new Request.Builder()\n          .url(\"https://api.github.com/search/repositories?q=http\")\n          .build();\n\n      OkHttpClient clientForCall;\n      if (i == 2) {\n        // Force this request's response to be written to the cache. This way, subsequent responses\n        // can be read from the cache.\n        System.out.println(\"Force cache: true\");\n        clientForCall = client.newBuilder()\n            .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)\n            .build();\n      } else {\n        System.out.println(\"Force cache: false\");\n        clientForCall = client;\n      }\n\n      try (Response response = clientForCall.newCall(request).execute()) {\n        if (!response.isSuccessful()) throw new IOException(\"Unexpected code \" + response);\n\n        System.out.println(\"    Network: \" + (response.networkResponse() != null));\n        System.out.println();\n      }\n    }\n  }\n\n  public static void main(String... args) throws Exception {\n    new RewriteResponseCacheControl(new File(\"RewriteResponseCacheControl.tmp\")).run();\n  }\n}\n","sourceCodeStart":50,"sourceCodeEnd":80,"githubUrl":"https://github.com/square/okhttp/blob/91a8b34c6f44bd28c421364f8edadc9f324dddd9/samples/guide/src/main/java/okhttp3/recipes/RewriteResponseCacheControl.java#L50-L80","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nRequest request = new Request.Builder()\n    .url(\"https://api.github.com/search/repositories?q=http\")\n    .build();\n...\nif (!response.isSuccessful()) throw new IOException(\"Unexpected code \" + response);\n\n// after\nRequest request = new Request.Builder()\n    .url(\"https://api.github.com/search/repositories?q=http\")\n    .header(\"Authorization\", \"token \" + System.getenv(\"GITHUB_TOKEN\"))\n    .header(\"Accept\", \"application/vnd.github+json\")\n    .build();\n...\nif (!response.isSuccessful()) {\n  throw new IOException(\"GitHub \" + response.code() + \" \" + response.message()\n      + \" (rate limit remaining: \" + response.header(\"X-RateLimit-Remaining\") + \")\");\n}","handlingStrategy":"validation","validationCode":"// Validate auth + rate-limit budget before the call.\nString token = System.getenv(\"GITHUB_TOKEN\");\nif (token == null || token.isBlank()) {\n  throw new IllegalStateException(\"GITHUB_TOKEN required for Search API without 403.\");\n}\n// Back off based on last-known remaining budget (read from a previous response).\nif (lastRemaining != null && lastRemaining <= 1) {\n  long sleepMs = Math.max(0, lastResetEpochMillis - System.currentTimeMillis());\n  Thread.sleep(sleepMs);\n}\nRequest request = new Request.Builder()\n    .url(\"https://api.github.com/search/repositories?q=http\")\n    .header(\"Authorization\", \"token \" + token)\n    .header(\"Accept\", \"application/vnd.github+json\")\n    .build();\n\n// After: capture rate-limit headers for the next iteration.\ntry (Response response = clientForCall.newCall(request).execute()) {\n  lastRemaining = Integer.valueOf(response.header(\"X-RateLimit-Remaining\", \"-1\"));\n  lastResetEpochMillis = Long.parseLong(response.header(\"X-RateLimit-Reset\", \"0\")) * 1000L;\n  if (response.code() == 403 && \"0\".equals(response.header(\"X-RateLimit-Remaining\"))) {\n    throw new RateLimitedException(\"GitHub rate limit exhausted; resets at \" + lastResetEpochMillis);\n  }\n  if (!response.isSuccessful()) throw new IOException(\"HTTP \" + response.code());\n}","typeGuard":"private static boolean isRateLimited(Response r) {\n  return r.code() == 403\n      && \"0\".equals(r.header(\"X-RateLimit-Remaining\"));\n}\nprivate static boolean isCacheUsable(Response r) {\n  return r.isSuccessful() && r.cacheResponse() != null;\n}","tryCatchPattern":"try (Response response = clientForCall.newCall(request).execute()) {\n  if (response.code() == 403 && isRateLimited(response)) {\n    // honor X-RateLimit-Reset, then retry the current iteration\n    throw new RateLimitedException(response.header(\"X-RateLimit-Reset\"));\n  }\n  if (!response.isSuccessful()) {\n    throw new IOException(\"GitHub \" + response.code() + \" \" + response.message());\n  }\n  System.out.println(\"Network: \" + (response.networkResponse() != null));\n} catch (RateLimitedException e) {\n  sleepUntilReset(e.resetEpochMillis());\n  i--; // redo this iteration\n}","preventionTips":["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."],"tags":["okhttp","java","http-status","rate-limit","github-api","caching"],"backgroundTag":null,"analyzedSha":"91a8b34c6f44bd28c421364f8edadc9f324dddd9","analyzedAt":"2026-08-10T18:39:54.316Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}