OkHttp is a battle-tested HTTP client that handles the messy parts of networking - connection pooling, GZIP compression, HTTP/2 multiplexing, and automatic retries. This guide shows you how to leverage OkHttp's full potential, from basic requests to advanced techniques like certificate pinning bypasses and connection pool tuning.
Why OkHttp Over Java's Native HTTPClient?
Before diving in, let's address the elephant in the room. Java 11+ ships with a solid HTTPClient, so why use OkHttp? Three reasons:
- Connection reuse magic - OkHttp's connection pooling is more aggressive and intelligent
- HTTP/2 server push support - Still experimental in Java's HTTPClient
- Android compatibility - Works seamlessly from API 21+
Step 1: Setup and First Request
Add OkHttp to your project. For Maven:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
For Gradle:
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
Here's your first request - but done right:
public class HttpService {
// Singleton pattern - NEVER create multiple clients
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
public String fetchData(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed: " + response.code());
}
return response.body().string();
}
}
}
Critical detail: That try-with-resources
block isn't optional. Forgetting to close the response body is the #1 cause of connection leaks in OkHttp applications.
Step 2: Smart Request Building
Most tutorials show you basic GET/POST. Let's skip that and go straight to the useful patterns.
Dynamic Headers with Request.Builder
public Response authenticatedRequest(String url, String token) throws IOException {
Request request = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + token)
.header("User-Agent", "MyApp/1.0")
.header("Accept-Encoding", "gzip, deflate") // OkHttp handles decompression
.build();
return client.newCall(request).execute();
}
Multipart File Upload (The Right Way)
public void uploadFile(File file, String endpoint) throws IOException {
// Calculate content length for progress tracking
long fileSize = file.length();
RequestBody fileBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/octet-stream");
}
@Override
public long contentLength() {
return fileSize;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(file)) {
sink.writeAll(source);
}
}
};
MultipartBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(), fileBody)
.addFormDataPart("metadata", "{\"uploaded_at\":\"" + Instant.now() + "\"}")
.build();
Request request = new Request.Builder()
.url(endpoint)
.post(requestBody)
.build();
client.newCall(request).execute();
}
Step 3: Interceptors - The Secret Weapon
Interceptors are where OkHttp shines. They let you modify requests and responses globally without touching business logic.
Automatic Retry Interceptor
Here's a production-ready retry interceptor that handles transient failures:
public class SmartRetryInterceptor implements Interceptor {
private final int maxRetries;
private final Set<Integer> retriableCodes = Set.of(408, 429, 500, 502, 503, 504);
public SmartRetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (response != null) {
response.close();
}
// Exponential backoff
if (attempt > 0) {
Thread.sleep((long) Math.pow(2, attempt) * 1000);
}
response = chain.proceed(request);
// Check if we should retry based on response code
if (!retriableCodes.contains(response.code()) || attempt == maxRetries) {
return response;
}
// Check for Retry-After header
String retryAfter = response.header("Retry-After");
if (retryAfter != null) {
Thread.sleep(Long.parseLong(retryAfter) * 1000);
}
} catch (IOException e) {
lastException = e;
if (attempt == maxRetries) {
throw e;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", e);
}
}
throw lastException != null ? lastException : new IOException("Max retries reached");
}
}
Rate Limiting Interceptor
Prevent hitting API rate limits:
public class RateLimitInterceptor implements Interceptor {
private final RateLimiter rateLimiter;
public RateLimitInterceptor(int requestsPerSecond) {
this.rateLimiter = RateLimiter.create(requestsPerSecond);
}
@Override
public Response intercept(Chain chain) throws IOException {
rateLimiter.acquire();
return chain.proceed(chain.request());
}
}
Wire them up:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new SmartRetryInterceptor(3))
.addInterceptor(new RateLimitInterceptor(10))
.build();
Step 4: Connection Pool Optimization
OkHttp's default connection pool works for most apps, but high-traffic services need tuning.
Custom Connection Pool
// Default: 5 idle connections, 5 minute keep-alive
ConnectionPool customPool = new ConnectionPool(
20, // Max idle connections
1, // Keep-alive duration
TimeUnit.MINUTES
);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(customPool)
.build();
Connection Pool Monitoring
Track pool health in production:
public class ConnectionPoolMonitor {
private final ConnectionPool pool;
public void logPoolStats() {
System.out.printf("Idle: %d, Total: %d%n",
pool.idleConnectionCount(),
pool.connectionCount());
}
public void evictIdleConnections() {
pool.evictAll(); // Force cleanup during low traffic
}
}
Pro tip: For microservices, use smaller pools (5-10 connections) with shorter keep-alives (10-30 seconds). For monoliths, go bigger (20-50 connections) with standard keep-alives (5 minutes).
Step 5: Certificate Pinning (And How to Bypass It)
Certificate pinning prevents MITM attacks by validating server certificates against known hashes.
Implementing Certificate Pinning
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("*.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(pinner)
.build();
To get the correct hash, intentionally break it first:
// Use a fake hash
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.github.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
// The error message will contain the actual hashes
Bypassing Certificate Pinning (For Testing)
During development, you need to test with local proxies. Here's how to disable pinning:
public class TrustAllCertificates {
public static OkHttpClient getUnsafeOkHttpClient() {
try {
// Trust manager that accepts everything
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier((hostname, session) -> true);
return builder.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Warning: Never ship this code to production. Use build flavors or environment checks:
OkHttpClient client = BuildConfig.DEBUG
? TrustAllCertificates.getUnsafeOkHttpClient()
: createSecureClient();
Step 6: WebSocket Support
OkHttp handles WebSockets elegantly:
public class WebSocketClient {
private WebSocket webSocket;
public void connect(String url) {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // Disable timeout for WebSocket
.build();
Request request = new Request.Builder()
.url(url)
.build();
WebSocketListener listener = new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
webSocket.send("Hello WebSocket!");
}
@Override
public void onMessage(WebSocket webSocket, String text) {
System.out.println("Received: " + text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
t.printStackTrace();
reconnect(); // Implement reconnection logic
}
};
webSocket = client.newWebSocket(request, listener);
}
private void reconnect() {
// Exponential backoff reconnection
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.schedule(() -> connect("wss://example.com/socket"), 5, TimeUnit.SECONDS);
}
}
Common Pitfalls and Solutions
1. Connection Leaks
Problem: Not closing response bodies leads to connection exhaustion.
Solution: Always use try-with-resources or explicitly close:
// Bad
Response response = client.newCall(request).execute();
String body = response.body().string(); // Connection leaked!
// Good
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
2. Creating Multiple Clients
Problem: Each OkHttpClient has its own connection pool, defeating pooling benefits.
Solution: Share a single client instance:
public class HttpClientFactory {
private static final OkHttpClient INSTANCE = createClient();
private static OkHttpClient createClient() {
return new OkHttpClient.Builder()
// Your configuration
.build();
}
public static OkHttpClient getInstance() {
return INSTANCE;
}
// For customization without losing pool benefits
public static OkHttpClient.Builder newBuilder() {
return INSTANCE.newBuilder();
}
}
3. Blocking on Async Calls
Problem: Using async incorrectly blocks threads:
// Wrong way
CountDownLatch latch = new CountDownLatch(1);
client.newCall(request).enqueue(new Callback() {
public void onResponse(Call call, Response response) {
// Process
latch.countDown();
}
});
latch.await(); // Blocks, defeating async purpose
Solution: Embrace async with CompletableFuture:
public CompletableFuture<String> asyncRequest(String url) {
CompletableFuture<String> future = new CompletableFuture<>();
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
future.completeExceptionally(e);
}
@Override
public void onResponse(Call call, Response response) {
try {
future.complete(response.body().string());
} catch (IOException e) {
future.completeExceptionally(e);
} finally {
response.close();
}
}
});
return future;
}
Performance Tips
- Enable HTTP/2: It's automatic for HTTPS connections, but verify with logging interceptor
- Use connection pooling: Never create clients per-request
- Configure timeouts appropriately: Long timeouts = thread starvation
- GZIP is automatic: Don't manually set Accept-Encoding
- Cache responses: Use OkHttp's cache, not custom solutions
// Response caching
File cacheDirectory = new File(context.getCacheDir(), "http-cache");
int cacheSize = 10 * 1024 * 1024; // 10 MB
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
Conclusion
OkHttp isn't just another HTTP client - it's a networking Swiss Army knife. While Java's native HTTPClient handles basics, OkHttp excels at the edge cases: flaky networks, high concurrency, and complex security requirements.
The key takeaways:
- One client to rule them all - Share instances for connection pooling
- Interceptors for cross-cutting concerns - Don't scatter retry logic
- Tune your pools - Defaults aren't always optimal
- Close your responses - Memory leaks kill performance
Start with the basics, but don't stop there. The real power of OkHttp lies in its advanced features - master them, and you'll handle any networking challenge Java throws at you.