square/okhttp · error · IOException

Unexpected code

Error message

Unexpected code 

What it means

Thrown in the certificate-pinning recipe: `if (!response.isSuccessful()) throw new IOException("Unexpected code " + response)`. Pinning validates the server's certificate chain against the hardcoded SHA-256 pin; if pinning fails OkHttp throws SSLPeerUnverifiedException during the handshake (a transport error -> onFailure / execute IOException), NOT this guard. This particular line only fires when the handshake SUCCEEDED (pin matched) but the HTTP status is non-2xx.

Solutions

  1. Distinguish the two failure classes: handshake/SSL errors mean a pin problem; this line means an HTTP status problem.
  2. Check response.code() to see the real status; 404 means the path is missing, not a TLS issue.
  3. Verify the pinned certificate is still current by reading peerCertificates from a successful response.
  4. Use a branch instead of a blanket throw so pinning demos and status errors are reported separately.

Example fix

// before
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

// after
if (!response.isSuccessful()) {
  throw new IOException("HTTP " + response.code()
      + " (handshake OK, pin=" + CertificatePinner.pin(response.handshake().peerCertificates().get(0)) + ")");
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Separate TLS errors from HTTP-status errors.
try (Response r = client.newCall(request).execute()) {
  if (!r.isSuccessful()) { /* HTTP status problem, not pinning */ }
} catch (SSLPeerUnverifiedException e) {
  // THIS is a pin mismatch; HTTP status never reached.
}

Type guard

static boolean pinMatches(Response r, String expectedPin) {
  return r.handshake() != null
    && CertificatePinner.pin(r.handshake().peerCertificates().get(0)).equals(expectedPin);
}

Try / catch

try {
  // call
} catch (SSLPeerUnverifiedException e) {
  // certificate pin mismatch (TLS layer)
} catch (IOException e) {
  // includes 'Unexpected code' (HTTP status) and other transport errors
}

Prevention

When it happens

Trigger: Handshake succeeds against https://publicobject.com/robots.txt (pin matched) but the server returns 404 (no robots.txt), 403, or 5xx. Note: a pin mismatch surfaces earlier as SSLPeerUnverifiedException and never reaches this line.

Common situations: The pinned host removed /robots.txt; the pin in the sample is stale and the server rotated its certificate (would fail at handshake, not here, but is commonly confused); copy-pasting the sample pin against a different host.

Related errors


AI-assisted analysis of square/okhttp@91a8b34c6f (2026-08-10). Data as JSON: /api/errors/70c1829c73e7c085. Report an issue: GitHub.

Appendix: source

Thrown at samples/guide/src/main/java/okhttp3/recipes/CertificatePinning.java:39

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public final class CertificatePinning {
  private final OkHttpClient client = new OkHttpClient.Builder()
      .certificatePinner(
          new CertificatePinner.Builder()
              .add("publicobject.com", "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=")
              .build())
      .build();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/robots.txt")
        .build();

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

      for (Certificate certificate : response.handshake().peerCertificates()) {
        System.out.println(CertificatePinner.pin(certificate));
      }
    }
  }

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

View on GitHub (pinned to 91a8b34c6f)