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
- Distinguish the two failure classes: handshake/SSL errors mean a pin problem; this line means an HTTP status problem.
- Check response.code() to see the real status; 404 means the path is missing, not a TLS issue.
- Verify the pinned certificate is still current by reading peerCertificates from a successful response.
- 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
- Distinguish SSLPeerUnverifiedException (pin/TLS) from HTTP-status IOException.
- Keep certificate pins current; rotate them when servers renew certs.
- Hold multiple pins (old + new) during certificate rotation.
- Derive pins from real peer certs, not from sample strings.
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)