square/okhttp · error · IOException
Unexpected code
Error message
Unexpected code
What it means
Recipe-level guard on the GET inside CheckHandshake. Because the denylist interceptor (error [6]) runs first, reaching this line means the peer certificate was NOT denylisted and chain.proceed() completed. If the resulting Response is non-2xx, java.io.IOException("Unexpected code " + response) is thrown. This is independent of the handshake check.
Source
Thrown at samples/guide/src/main/java/okhttp3/recipes/CheckHandshake.java:55
if (denylist.contains(pin)) {
throw new IOException("Denylisted peer certificate: " + pin);
}
}
return chain.proceed(chain.request());
}
};
private final OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(CHECK_HANDSHAKE_INTERCEPTOR)
.build();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
public static void main(String... args) throws Exception {
new CheckHandshake().run();
}
}
View on GitHub (pinned to 4fc0831380)
Solutions
- Inspect response.code() — it is a normal HTTP status, unrelated to certificate checking.
- Keep denylist handling (error [6]) and HTTP-status handling (this line) in separate catch branches.
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.isSuccessful()) {
throw new IOException("HTTP " + response.code() + " (handshake passed, denylist OK)");
}
...
} Defensive patterns
Strategy: validation
Validate before calling
// Separate HTTP-status handling from the handshake/denylist policy
// After the call returns (handshake + denylist passed), branch on code:
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
errors.accept(new HttpException(response.code(), response.request().url().toString()));
return;
}
// use body
} Try / catch
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
// handshake passed AND denylist passed; this is a plain HTTP error
throw new HttpException(response.code(), response.message());
}
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Denylisted")) {
throw new SecurityPolicyException(e);
}
throw e;
} Prevention
- Keep denylist policy handling and HTTP-status handling in distinct branches.
- Do not conflate reaching this line with a handshake problem.
- Inspect response.code() to determine the actual HTTP failure.
When it happens
Trigger: GET https://publicobject.com/helloworld.txt after the handshake interceptor passed. Fires for ordinary non-2xx (404, 503, etc.). A denylisted cert would have thrown at the interceptor, never reaching here.
Common situations: Conflating this line with the denylist error above; the origin returning 5xx while users blame the handshake interceptor.
Related errors
AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04).
Data as JSON: /data/errors/5fd4a849cb433425.json.
Report an issue: GitHub.