square/okhttp · error · IOException

Unexpected code

Error message

Unexpected code 

What it means

This is a sample-recipe guard (not built into OkHttp): after client.newCall(request).execute() returns a Response, the code checks response.isSuccessful() (true only for HTTP status 200-299) and throws java.io.IOException("Unexpected code " + response) for anything else. The Response.toString() appended to the message contains the full status line, URL, and headers, which is the only diagnostic you get. OkHttp itself never throws this — it is the recipe author's convention for turning a transport-success-but-HTTP-error into an exception.

Source

Thrown at samples/guide/src/main/java/okhttp3/recipes/AccessHeaders.java:35

import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public final class AccessHeaders {
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/lysine-dev/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println("Server: " + response.header("Server"));
      System.out.println("Date: " + response.header("Date"));
      System.out.println("Vary: " + response.headers("Vary"));
    }
  }

  public static void main(String... args) throws Exception {
    new AccessHeaders().run();
  }
}

View on GitHub (pinned to 4fc0831380)

Solutions

  1. Inspect response.code() and response.headers() (X-RateLimit-Remaining, X-RateLimit-Reset) before throwing to see the real cause.
  2. If 403 rate-limited, authenticate the request with a GitHub token (Authorization: Bearer <token>) to raise the limit, or add caching/backoff.
  3. If 404, verify the repo owner/name in the URL is correct (the recipe pins lysine-dev/okhttp).
  4. Keep the User-Agent header (recipe sets 'OkHttp Headers.java'); GitHub requires a non-empty UA.
  5. Branch on response.isSuccessful() instead of throwing, so 3xx/4xx/5xx can be handled individually.

Example fix

// before
try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  ...
}

// after
try (Response response = client.newCall(request).execute()) {
  if (response.code() == 403) {
    throw new IOException("GitHub rate-limited; remaining="
        + response.header("X-RateLimit-Remaining"));
  }
  if (!response.isSuccessful()) {
    throw new IOException("GitHub " + response.code() + " for " + response.request().url());
  }
  ...
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the GitHub target before relying on the response
Request request = new Request.Builder()
    .url("https://api.github.com/repos/lysine-dev/okhttp/issues")
    .header("User-Agent", "MyApp/1.0 (contact@example.com)")  // GitHub requires this
    .header("Authorization", "Bearer " + token)                // lifts 60/hr cap
    .header("Accept", "application/vnd.github+json")
    .build();
try (Response response = client.newCall(request).execute()) {
  if (response.code() == 403 || response.code() == 429) {
    // respect X-RateLimit-Reset before retrying
  }
  if (!response.isSuccessful()) { /* handle by code */ }
}

Try / catch

try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) {
    switch (response.code()) {
      case 403: throw new RateLimitedException(response.header("X-RateLimit-Reset"));
      case 404: throw new NotFoundException(response.request().url().toString());
      default:  throw new HttpException(response.code(), response.message());
    }
  }
  // use response
}

Prevention

When it happens

Trigger: GET https://api.github.com/repos/lysine-dev/okhttp/issues with custom Accept + User-Agent headers. Surfaces when GitHub returns 404 (repo renamed/deleted/non-existent), 301/302 the recipe does not follow (it will follow by default), 403 (unauthenticated rate limit exceeded — 60 req/hr per IP), or 410. response.code() will be the actual failing status.

Common situations: Hitting GitHub's unauthenticated rate limit (403 with X-RateLimit-Remaining: 0); copying the recipe but forgetting the User-Agent header (GitHub rejects requests without one); targeting a repo slug that does not exist or was renamed; running behind a shared NAT IP where the 60/hr limit is consumed by others.

Related errors


AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04). Data as JSON: /data/errors/cb977d990cba03b3.json. Report an issue: GitHub.